feat(refresh): Lottie 새로고침 표시를 적용한다
This commit is contained in:
@@ -387,6 +387,14 @@
|
||||
405C20942D07FC6300804C0B /* UpdatePlaylistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405C20922D07FC6300804C0B /* UpdatePlaylistRequest.swift */; };
|
||||
405C3FB32D78AB7900A1551A /* AppsFlyerLib-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 405C3FB22D78AB7900A1551A /* AppsFlyerLib-Static */; };
|
||||
405C3FB52D78AB8900A1551A /* AppsFlyerLib-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 405C3FB42D78AB8900A1551A /* AppsFlyerLib-Static */; };
|
||||
405EC8BB303D97F30037B269 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 405EC8BA303D97F30037B269 /* Lottie */; };
|
||||
405EC8BD303D98100037B269 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 405EC8BC303D98100037B269 /* Lottie */; };
|
||||
405EC8BF303D99D20037B269 /* pull-to-refresh.json in Resources */ = {isa = PBXBuildFile; fileRef = 405EC8BE303D99D20037B269 /* pull-to-refresh.json */; };
|
||||
405EC8C0303D99D20037B269 /* pull-to-refresh.json in Resources */ = {isa = PBXBuildFile; fileRef = 405EC8BE303D99D20037B269 /* pull-to-refresh.json */; };
|
||||
A4F100000000000000000001 /* LottieRefreshIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F100000000000000000005 /* LottieRefreshIndicator.swift */; };
|
||||
A4F100000000000000000002 /* LottieRefreshIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F100000000000000000005 /* LottieRefreshIndicator.swift */; };
|
||||
A4F100000000000000000003 /* LottieRefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F100000000000000000006 /* LottieRefreshableScrollView.swift */; };
|
||||
A4F100000000000000000004 /* LottieRefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F100000000000000000006 /* LottieRefreshableScrollView.swift */; };
|
||||
405F791B2D2B84D70037DE3A /* AuditionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791A2D2B84D70037DE3A /* AuditionItemView.swift */; };
|
||||
405F791C2D2B84D70037DE3A /* AuditionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791A2D2B84D70037DE3A /* AuditionItemView.swift */; };
|
||||
405F791F2D2BA3140037DE3A /* AuditionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791E2D2BA3140037DE3A /* AuditionDetailView.swift */; };
|
||||
@@ -2311,6 +2319,9 @@
|
||||
405C208C2D07FBC400804C0B /* ContentPlaylistModifyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaylistModifyView.swift; sourceTree = "<group>"; };
|
||||
405C208F2D07FBEC00804C0B /* ContentPlaylistModifyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaylistModifyViewModel.swift; sourceTree = "<group>"; };
|
||||
405C20922D07FC6300804C0B /* UpdatePlaylistRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePlaylistRequest.swift; sourceTree = "<group>"; };
|
||||
405EC8BE303D99D20037B269 /* pull-to-refresh.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pull-to-refresh.json"; sourceTree = "<group>"; };
|
||||
A4F100000000000000000005 /* LottieRefreshIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieRefreshIndicator.swift; sourceTree = "<group>"; };
|
||||
A4F100000000000000000006 /* LottieRefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieRefreshableScrollView.swift; sourceTree = "<group>"; };
|
||||
405F791A2D2B84D70037DE3A /* AuditionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionItemView.swift; sourceTree = "<group>"; };
|
||||
405F791E2D2BA3140037DE3A /* AuditionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionDetailView.swift; sourceTree = "<group>"; };
|
||||
405F79212D2BA3230037DE3A /* AuditionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionDetailViewModel.swift; sourceTree = "<group>"; };
|
||||
@@ -3141,6 +3152,7 @@
|
||||
753741799DEF278DC4C515AA /* KakaoSDKAuth in Frameworks */,
|
||||
94E04DD7ABB0BBF450352E21 /* KakaoSDKUser in Frameworks */,
|
||||
DD202E48D61F1CE8108AD164 /* LineSDK in Frameworks */,
|
||||
405EC8BB303D97F30037B269 /* Lottie in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -3171,6 +3183,7 @@
|
||||
3F829933ED2755E9F0BBC5FE /* KakaoSDKAuth in Frameworks */,
|
||||
C8B170409F057CA2B8501362 /* KakaoSDKUser in Frameworks */,
|
||||
A1465C799E10028802BD2A68 /* LineSDK in Frameworks */,
|
||||
405EC8BD303D98100037B269 /* Lottie in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -5481,6 +5494,7 @@
|
||||
40FE0E962A83622D00252293 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
405EC8BE303D99D20037B269 /* pull-to-refresh.json */,
|
||||
402830D62E8ABF2E001A6577 /* payverse_starter.html */,
|
||||
402830D72E8ABF2E001A6577 /* payverse_starter_debug.html */,
|
||||
40FE0EAD2A83642C00252293 /* Debug */,
|
||||
@@ -5642,6 +5656,7 @@
|
||||
F7F1421DBAB56E2F6891625E /* DefaultTitleBar.swift */,
|
||||
DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */,
|
||||
25C5D0734F54C0ED7B7BD462 /* Modal */,
|
||||
A4F100000000000000000007 /* Refresh */,
|
||||
D16C12DFB83FE164B38CC066 /* SectionTitle.swift */,
|
||||
0FD267162A809D893E2E3B3F /* Text */,
|
||||
E61608194B084224DB6B78B5 /* TextTabBar.swift */,
|
||||
@@ -5650,6 +5665,15 @@
|
||||
path = Component;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4F100000000000000000007 /* Refresh */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4F100000000000000000005 /* LottieRefreshIndicator.swift */,
|
||||
A4F100000000000000000006 /* LottieRefreshableScrollView.swift */,
|
||||
);
|
||||
path = Refresh;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AA4588B91C0F73344C3DC4A4 /* FanTalk */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -6413,6 +6437,7 @@
|
||||
8069DA64D0A280105DF612B6 /* KakaoSDKAuth */,
|
||||
061FD682D7FE09325C4B460A /* KakaoSDKUser */,
|
||||
6DBA0C325ACEC3FCDB22490D /* LineSDK */,
|
||||
405EC8BA303D97F30037B269 /* Lottie */,
|
||||
);
|
||||
productName = SodaLive;
|
||||
productReference = 40FE0E842A8361DF00252293 /* SodaLive.app */;
|
||||
@@ -6456,6 +6481,7 @@
|
||||
E9CD5F29965B148AB360C353 /* KakaoSDKAuth */,
|
||||
927A707B5035B0A77C0F5EA6 /* KakaoSDKUser */,
|
||||
867D78B93CBC5738B7D43CEA /* LineSDK */,
|
||||
405EC8BC303D98100037B269 /* Lottie */,
|
||||
);
|
||||
productName = SodaLive;
|
||||
productReference = 40FE0EAC2A8362A200252293 /* SodaLive-dev.app */;
|
||||
@@ -6504,6 +6530,7 @@
|
||||
404DCB3D2D9A6FCC000935C5 /* XCRemoteSwiftPackageReference "objectbox-swift-spm" */,
|
||||
534A307BA769D6BC2D6343CB /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */,
|
||||
7D0E5FAF7AD538D1C2FED682 /* XCRemoteSwiftPackageReference "line-sdk-ios-swift" */,
|
||||
405EC8B9303D97F30037B269 /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||
);
|
||||
productRefGroup = 40FE0E852A8361DF00252293 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -6533,6 +6560,7 @@
|
||||
40B640742E20BA6300D0E4E4 /* Pretendard-Light.otf in Resources */,
|
||||
40B640752E20BA6300D0E4E4 /* Pretendard-Bold.otf in Resources */,
|
||||
402830D82E8ABF2E001A6577 /* payverse_starter_debug.html in Resources */,
|
||||
405EC8BF303D99D20037B269 /* pull-to-refresh.json in Resources */,
|
||||
402830D92E8ABF2E001A6577 /* payverse_starter.html in Resources */,
|
||||
40FE0E8C2A8361E100252293 /* Assets.xcassets in Resources */,
|
||||
40A5BA882A8B38CC004A4635 /* GoogleService-Info.plist in Resources */,
|
||||
@@ -6556,6 +6584,7 @@
|
||||
40B640702E20BA6300D0E4E4 /* Pretendard-Light.otf in Resources */,
|
||||
40B640712E20BA6300D0E4E4 /* Pretendard-Bold.otf in Resources */,
|
||||
402830DA2E8ABF2E001A6577 /* payverse_starter_debug.html in Resources */,
|
||||
405EC8C0303D99D20037B269 /* pull-to-refresh.json in Resources */,
|
||||
402830DB2E8ABF2E001A6577 /* payverse_starter.html in Resources */,
|
||||
40FE0EA62A8362A200252293 /* Assets.xcassets in Resources */,
|
||||
40728D522D79780A00D2C580 /* GoogleService-Info.plist in Resources */,
|
||||
@@ -6722,6 +6751,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A4F100000000000000000001 /* LottieRefreshIndicator.swift in Sources */,
|
||||
A4F100000000000000000003 /* LottieRefreshableScrollView.swift in Sources */,
|
||||
400EDE592B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
||||
CC0101012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */,
|
||||
CC0101032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */,
|
||||
@@ -7822,6 +7853,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A4F100000000000000000002 /* LottieRefreshIndicator.swift in Sources */,
|
||||
A4F100000000000000000004 /* LottieRefreshableScrollView.swift in Sources */,
|
||||
400EDE5A2B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
||||
409E35092B0759DE00CCB985 /* MoyaProviderExtension.swift in Sources */,
|
||||
40DDBD782B2B4FD700A2E8DA /* CreatorCommunityMoreItemView.swift in Sources */,
|
||||
@@ -9130,6 +9163,14 @@
|
||||
minimumVersion = 6.16.1;
|
||||
};
|
||||
};
|
||||
405EC8B9303D97F30037B269 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.6.1;
|
||||
};
|
||||
};
|
||||
406AEAA12D6D9CCE00D1AEFE /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/facebook/facebook-ios-sdk.git";
|
||||
@@ -9279,6 +9320,16 @@
|
||||
package = 405C3FB12D78AB7900A1551A /* XCRemoteSwiftPackageReference "AppsFlyerFramework-Static" */;
|
||||
productName = "AppsFlyerLib-Static";
|
||||
};
|
||||
405EC8BA303D97F30037B269 /* Lottie */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 405EC8B9303D97F30037B269 /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||
productName = Lottie;
|
||||
};
|
||||
405EC8BC303D98100037B269 /* Lottie */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 405EC8B9303D97F30037B269 /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||
productName = Lottie;
|
||||
};
|
||||
406AEAA22D6D9CCE00D1AEFE /* FacebookCore */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 406AEAA12D6D9CCE00D1AEFE /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */;
|
||||
|
||||
@@ -1,303 +1,312 @@
|
||||
{
|
||||
"originHash": "1f28da3687662a2a9efe60ffc2ca2499be411b5b0a1e07f72559059c40728121",
|
||||
"pins": [
|
||||
"originHash" : "879f32da6c663e9c4a413e005f26682b94e0dfa4995d4a5b8119800465a10127",
|
||||
"pins" : [
|
||||
{
|
||||
"identity": "abseil-cpp-binary",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state": {
|
||||
"revision": "194a6706acbd25e4ef639bcaddea16e8758a3e27",
|
||||
"version": "1.2024011602.0"
|
||||
"identity" : "abseil-cpp-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state" : {
|
||||
"revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27",
|
||||
"version" : "1.2024011602.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "agorainfra_ios",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/AgoraIO/AgoraInfra_iOS.git",
|
||||
"state": {
|
||||
"revision": "0071cecee99160f056c943ee6bfca06685ffa3d7",
|
||||
"version": "1.3.0"
|
||||
"identity" : "agorainfra_ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/AgoraIO/AgoraInfra_iOS.git",
|
||||
"state" : {
|
||||
"revision" : "0071cecee99160f056c943ee6bfca06685ffa3d7",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "agorartcengine_ios",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/AgoraIO/AgoraRtcEngine_iOS.git",
|
||||
"state": {
|
||||
"revision": "c1224ad3e316db15a4ac48a6ff08208f7bcf5238",
|
||||
"version": "4.6.0"
|
||||
"identity" : "agorartcengine_ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/AgoraIO/AgoraRtcEngine_iOS.git",
|
||||
"state" : {
|
||||
"revision" : "c1224ad3e316db15a4ac48a6ff08208f7bcf5238",
|
||||
"version" : "4.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "alamofire",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/Alamofire/Alamofire.git",
|
||||
"state": {
|
||||
"revision": "513364f870f6bfc468f9d2ff0a95caccc10044c5",
|
||||
"version": "5.10.2"
|
||||
"identity" : "alamofire",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
|
||||
"version" : "5.10.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "app-check",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/app-check.git",
|
||||
"state": {
|
||||
"revision": "3b62f154d00019ae29a71e9738800bb6f18b236d",
|
||||
"version": "10.19.2"
|
||||
"identity" : "app-check",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "3b62f154d00019ae29a71e9738800bb6f18b236d",
|
||||
"version" : "10.19.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "appsflyerframework-static",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static",
|
||||
"state": {
|
||||
"revision": "1741d025d5bdd8a64c42854ba0fcfd7f768e4594",
|
||||
"version": "6.17.5"
|
||||
"identity" : "appsflyerframework-static",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static",
|
||||
"state" : {
|
||||
"revision" : "1741d025d5bdd8a64c42854ba0fcfd7f768e4594",
|
||||
"version" : "6.17.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "facebook-ios-sdk",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/facebook/facebook-ios-sdk.git",
|
||||
"state": {
|
||||
"revision": "a77ba210bf6534564ad4027fce2fef65babfadf8",
|
||||
"version": "18.0.1"
|
||||
"identity" : "facebook-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/facebook/facebook-ios-sdk.git",
|
||||
"state" : {
|
||||
"revision" : "a77ba210bf6534564ad4027fce2fef65babfadf8",
|
||||
"version" : "18.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "firebase-ios-sdk",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/firebase/firebase-ios-sdk.git",
|
||||
"state": {
|
||||
"revision": "eca84fd638116dd6adb633b5a3f31cc7befcbb7d",
|
||||
"version": "10.29.0"
|
||||
"identity" : "firebase-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk.git",
|
||||
"state" : {
|
||||
"revision" : "eca84fd638116dd6adb633b5a3f31cc7befcbb7d",
|
||||
"version" : "10.29.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "googleappmeasurement",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state": {
|
||||
"revision": "fe727587518729046fc1465625b9afd80b5ab361",
|
||||
"version": "10.28.0"
|
||||
"identity" : "googleappmeasurement",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "fe727587518729046fc1465625b9afd80b5ab361",
|
||||
"version" : "10.28.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "googledatatransport",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/GoogleDataTransport.git",
|
||||
"state": {
|
||||
"revision": "a637d318ae7ae246b02d7305121275bc75ed5565",
|
||||
"version": "9.4.0"
|
||||
"identity" : "googledatatransport",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||
"state" : {
|
||||
"revision" : "a637d318ae7ae246b02d7305121275bc75ed5565",
|
||||
"version" : "9.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "googleutilities",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/GoogleUtilities.git",
|
||||
"state": {
|
||||
"revision": "57a1d307f42df690fdef2637f3e5b776da02aad6",
|
||||
"version": "7.13.3"
|
||||
"identity" : "googleutilities",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6",
|
||||
"version" : "7.13.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "grpc-binary",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/grpc-binary.git",
|
||||
"state": {
|
||||
"revision": "e9fad491d0673bdda7063a0341fb6b47a30c5359",
|
||||
"version": "1.62.2"
|
||||
"identity" : "grpc-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/grpc-binary.git",
|
||||
"state" : {
|
||||
"revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359",
|
||||
"version" : "1.62.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "gtm-session-fetcher",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state": {
|
||||
"revision": "a2ab612cb980066ee56d90d60d8462992c07f24b",
|
||||
"version": "3.5.0"
|
||||
"identity" : "gtm-session-fetcher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
|
||||
"version" : "3.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "interop-ios-for-google-sdks",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state": {
|
||||
"revision": "2d12673670417654f08f5f90fdd62926dc3a2648",
|
||||
"version": "100.0.0"
|
||||
"identity" : "interop-ios-for-google-sdks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state" : {
|
||||
"revision" : "2d12673670417654f08f5f90fdd62926dc3a2648",
|
||||
"version" : "100.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "kakao-ios-sdk",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/kakao/kakao-ios-sdk",
|
||||
"state": {
|
||||
"revision": "5978979157a5a0521c9c56fd0156aec794caa21c",
|
||||
"version": "2.27.2"
|
||||
"identity" : "kakao-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kakao/kakao-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "5978979157a5a0521c9c56fd0156aec794caa21c",
|
||||
"version" : "2.27.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "kingfisher",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"revision": "2ef543ee21d63734e1c004ad6c870255e8716c50",
|
||||
"version": "7.12.0"
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher.git",
|
||||
"state" : {
|
||||
"revision" : "2ef543ee21d63734e1c004ad6c870255e8716c50",
|
||||
"version" : "7.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "leveldb",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/firebase/leveldb.git",
|
||||
"state": {
|
||||
"revision": "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||
"version": "1.22.5"
|
||||
"identity" : "leveldb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/leveldb.git",
|
||||
"state" : {
|
||||
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||
"version" : "1.22.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "line-sdk-ios-swift",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/line/line-sdk-ios-swift.git",
|
||||
"state": {
|
||||
"revision": "51ef2ebefb05db8f748e80208b3281ca723abcdb",
|
||||
"version": "5.14.0"
|
||||
"identity" : "line-sdk-ios-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/line/line-sdk-ios-swift.git",
|
||||
"state" : {
|
||||
"revision" : "51ef2ebefb05db8f748e80208b3281ca723abcdb",
|
||||
"version" : "5.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "moya",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/Moya/Moya.git",
|
||||
"state": {
|
||||
"revision": "c263811c1f3dbf002be9bd83107f7cdc38992b26",
|
||||
"version": "15.0.3"
|
||||
"identity" : "lottie-spm",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/airbnb/lottie-spm.git",
|
||||
"state" : {
|
||||
"revision" : "0aff16288f8dc1e3dab35cbf5cf45979673fd3df",
|
||||
"version" : "4.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "nanopb",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/firebase/nanopb.git",
|
||||
"state": {
|
||||
"revision": "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
||||
"version": "2.30910.0"
|
||||
"identity" : "moya",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Moya/Moya.git",
|
||||
"state" : {
|
||||
"revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
|
||||
"version" : "15.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "notifly-ios-sdk",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/team-michael/notifly-ios-sdk",
|
||||
"state": {
|
||||
"revision": "406d95344b7adb71adcc9e46960fec50d608e576",
|
||||
"version": "1.17.3"
|
||||
"identity" : "nanopb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
||||
"version" : "2.30910.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "objectbox-swift-spm",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/objectbox/objectbox-swift-spm",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "28c3261c9836cd3f4d64ab6419a3628d2b167811"
|
||||
"identity" : "notifly-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/team-michael/notifly-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "406d95344b7adb71adcc9e46960fec50d608e576",
|
||||
"version" : "1.17.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "popupview",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/exyte/PopupView.git",
|
||||
"state": {
|
||||
"revision": "1b99d6e9872ef91fd57aaef657661b5a00069638",
|
||||
"version": "1.3.1"
|
||||
"identity" : "objectbox-swift-spm",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/objectbox/objectbox-swift-spm",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "28c3261c9836cd3f4d64ab6419a3628d2b167811"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "promises",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/google/promises.git",
|
||||
"state": {
|
||||
"revision": "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
||||
"version": "2.4.0"
|
||||
"identity" : "popupview",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/exyte/PopupView.git",
|
||||
"state" : {
|
||||
"revision" : "1b99d6e9872ef91fd57aaef657661b5a00069638",
|
||||
"version" : "1.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "reactiveswift",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/ReactiveCocoa/ReactiveSwift.git",
|
||||
"state": {
|
||||
"revision": "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
|
||||
"version": "6.7.0"
|
||||
"identity" : "promises",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
||||
"version" : "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "refreshablescrollview",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/phuhuynh2411/RefreshableScrollView",
|
||||
"state": {
|
||||
"revision": "e06edf5dc4facc7fbf71179e8a94f0d1c7035ce3",
|
||||
"version": "1.1.1"
|
||||
"identity" : "reactiveswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git",
|
||||
"state" : {
|
||||
"revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
|
||||
"version" : "6.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "richtext",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/NuPlay/RichText.git",
|
||||
"state": {
|
||||
"revision": "ec3469ee47c17e0f90e4b43f23d7e19bc42e069b",
|
||||
"version": "2.7.0"
|
||||
"identity" : "refreshablescrollview",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/phuhuynh2411/RefreshableScrollView",
|
||||
"state" : {
|
||||
"revision" : "e06edf5dc4facc7fbf71179e8a94f0d1c7035ce3",
|
||||
"version" : "1.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "rxswift",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/ReactiveX/RxSwift.git",
|
||||
"state": {
|
||||
"revision": "5dd1907d64f0d36f158f61a466bab75067224893",
|
||||
"version": "6.9.0"
|
||||
"identity" : "richtext",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/NuPlay/RichText.git",
|
||||
"state" : {
|
||||
"revision" : "ec3469ee47c17e0f90e4b43f23d7e19bc42e069b",
|
||||
"version" : "2.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "sdwebimage",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state": {
|
||||
"revision": "34cf2423a2c4088d06a3b08655603b5bc3eeeb3a",
|
||||
"version": "5.21.2"
|
||||
"identity" : "rxswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ReactiveX/RxSwift.git",
|
||||
"state" : {
|
||||
"revision" : "5dd1907d64f0d36f158f61a466bab75067224893",
|
||||
"version" : "6.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "sdwebimageswiftui",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
|
||||
"state": {
|
||||
"revision": "53573d6dd017e354c0e7d8f1c86b77ef1383c996",
|
||||
"version": "2.2.7"
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state" : {
|
||||
"revision" : "34cf2423a2c4088d06a3b08655603b5bc3eeeb3a",
|
||||
"version" : "5.21.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "swift-protobuf",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/apple/swift-protobuf.git",
|
||||
"state": {
|
||||
"revision": "2547102afd04fe49f1b286090f13ebce07284980",
|
||||
"version": "1.31.1"
|
||||
"identity" : "sdwebimageswiftui",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
|
||||
"state" : {
|
||||
"revision" : "53573d6dd017e354c0e7d8f1c86b77ef1383c996",
|
||||
"version" : "2.2.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "swiftui-flow-layout",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/globulus/swiftui-flow-layout",
|
||||
"state": {
|
||||
"revision": "de7da3440c3b87ba94adfa98c698828d7746a76d",
|
||||
"version": "1.0.5"
|
||||
"identity" : "swift-protobuf",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||
"state" : {
|
||||
"revision" : "2547102afd04fe49f1b286090f13ebce07284980",
|
||||
"version" : "1.31.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity": "swiftui-sliders",
|
||||
"kind": "remoteSourceControl",
|
||||
"location": "https://github.com/spacenation/swiftui-sliders.git",
|
||||
"state": {
|
||||
"revision": "d5a7d856655d5c91f891c2b69d982c30fd5c7bdf",
|
||||
"version": "2.1.0"
|
||||
"identity" : "swiftui-flow-layout",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/globulus/swiftui-flow-layout",
|
||||
"state" : {
|
||||
"revision" : "de7da3440c3b87ba94adfa98c698828d7746a76d",
|
||||
"version" : "1.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-sliders",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/spacenation/swiftui-sliders.git",
|
||||
"state" : {
|
||||
"revision" : "d5a7d856655d5c91f891c2b69d982c30fd5c7bdf",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": 3
|
||||
"version" : 3
|
||||
}
|
||||
|
||||
213
SodaLive/Resources/pull-to-refresh.json
Normal file
213
SodaLive/Resources/pull-to-refresh.json
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"v": "5.9.0",
|
||||
"fr": 30,
|
||||
"ip": 0,
|
||||
"op": 45,
|
||||
"w": 150,
|
||||
"h": 150,
|
||||
"nm": "loading",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "loading",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 11
|
||||
},
|
||||
"r": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.667], "y": [1] },
|
||||
"o": { "x": [0.333], "y": [0] },
|
||||
"t": 0,
|
||||
"s": [-90]
|
||||
},
|
||||
{
|
||||
"t": 45,
|
||||
"s": [270]
|
||||
}
|
||||
],
|
||||
"ix": 10
|
||||
},
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [75, 75, 0],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [0, 0, 0],
|
||||
"ix": 1,
|
||||
"l": 2
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [100, 100, 100],
|
||||
"ix": 6,
|
||||
"l": 2
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"d": 1,
|
||||
"ty": "el",
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [100, 100],
|
||||
"ix": 2
|
||||
},
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 3
|
||||
},
|
||||
"nm": "타원 패스 1",
|
||||
"mn": "ADBE Vector Shape - Ellipse",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [1, 1, 1, 1],
|
||||
"ix": 3
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 4
|
||||
},
|
||||
"w": {
|
||||
"a": 0,
|
||||
"k": 20,
|
||||
"ix": 5
|
||||
},
|
||||
"lc": 2,
|
||||
"lj": 1,
|
||||
"ml": 4,
|
||||
"bm": 0,
|
||||
"nm": "선 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 1
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [100, 100],
|
||||
"ix": 3
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 6
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 7
|
||||
},
|
||||
"sk": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 4
|
||||
},
|
||||
"sa": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 5
|
||||
},
|
||||
"nm": "변형"
|
||||
}
|
||||
],
|
||||
"nm": "타원 1",
|
||||
"np": 3,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tm",
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.667], "y": [1] },
|
||||
"o": { "x": [0.333], "y": [0] },
|
||||
"t": 19.78,
|
||||
"s": [3]
|
||||
},
|
||||
{
|
||||
"t": 45,
|
||||
"s": [97]
|
||||
}
|
||||
],
|
||||
"ix": 1
|
||||
},
|
||||
"e": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.667], "y": [1] },
|
||||
"o": { "x": [0.333], "y": [0] },
|
||||
"t": 0,
|
||||
"s": [3]
|
||||
},
|
||||
{
|
||||
"i": { "x": [0.833], "y": [1] },
|
||||
"o": { "x": [0.167], "y": [0] },
|
||||
"t": 26.372,
|
||||
"s": [100]
|
||||
},
|
||||
{
|
||||
"t": 45,
|
||||
"s": [97]
|
||||
}
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 3
|
||||
},
|
||||
"m": 1,
|
||||
"ix": 2,
|
||||
"nm": "패스 다듬어 자르기 1",
|
||||
"mn": "ADBE Vector Filter - Trim",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 45,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import SwiftUI
|
||||
|
||||
import Lottie
|
||||
|
||||
struct LottieRefreshIndicator: View {
|
||||
let isAnimating: Bool
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var accessibilityReduceMotion
|
||||
|
||||
var body: some View {
|
||||
let animation = LottieView(animation: .named("pull-to-refresh"))
|
||||
.configure(\.contentMode, to: .scaleAspectFit)
|
||||
.resizable()
|
||||
|
||||
Group {
|
||||
if accessibilityReduceMotion {
|
||||
animation.currentFrame(1)
|
||||
} else if isAnimating {
|
||||
animation.looping()
|
||||
} else {
|
||||
animation.currentFrame(0)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: SodaSpacing.s48, maxHeight: SodaSpacing.s48)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LottieRefreshableScrollView<Content: View>: View {
|
||||
private let action: () async -> Void
|
||||
private let content: () -> Content
|
||||
private let refreshRegionHeight = SodaSpacing.s48 + (SodaSpacing.s8 * 2)
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var accessibilityReduceMotion
|
||||
@State private var pullOffset: CGFloat = 0
|
||||
@State private var isRefreshArmed = false
|
||||
@State private var isRefreshing = false
|
||||
|
||||
init(
|
||||
action: @escaping () async -> Void,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.action = action
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
pullOffsetReader
|
||||
refreshRegion
|
||||
content()
|
||||
}
|
||||
}
|
||||
.scrollBounceBehavior(.always, axes: .vertical)
|
||||
.background(PullOffsetPositionReader(type: .fixed))
|
||||
.onPreferenceChange(PullOffsetPreferenceKey.self) { positions in
|
||||
guard
|
||||
let movingMinY = positions.first(where: { $0.type == .moving })?.minY,
|
||||
let fixedMinY = positions.first(where: { $0.type == .fixed })?.minY
|
||||
else { return }
|
||||
|
||||
let offset = max(0, movingMinY - fixedMinY)
|
||||
pullOffset = offset
|
||||
|
||||
guard !isRefreshing else { return }
|
||||
|
||||
if offset >= refreshRegionHeight {
|
||||
isRefreshArmed = true
|
||||
} else if isRefreshArmed {
|
||||
isRefreshArmed = false
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
.accessibilityAction(named: Text(I18n.LiveNow.refreshButton)) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private var pullOffsetReader: some View {
|
||||
PullOffsetPositionReader(type: .moving)
|
||||
.frame(height: 0)
|
||||
}
|
||||
|
||||
private var refreshRegion: some View {
|
||||
Color.clear
|
||||
.frame(height: isRefreshing ? refreshRegionHeight : 0)
|
||||
.overlay(alignment: .top) {
|
||||
if pullOffset > 0 || isRefreshing {
|
||||
LottieRefreshIndicator(isAnimating: true)
|
||||
.padding(.vertical, SodaSpacing.s8)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: refreshRegionHeight)
|
||||
.offset(y: isRefreshing ? 0 : -refreshRegionHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refresh() {
|
||||
isRefreshArmed = false
|
||||
guard !isRefreshing else { return }
|
||||
|
||||
withAnimation(accessibilityReduceMotion ? nil : .easeOut(duration: 0.2)) {
|
||||
isRefreshing = true
|
||||
}
|
||||
Task { @MainActor in
|
||||
await action()
|
||||
withAnimation(accessibilityReduceMotion ? nil : .easeOut(duration: 0.2)) {
|
||||
pullOffset = 0
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum PullOffsetPositionType {
|
||||
case fixed
|
||||
case moving
|
||||
}
|
||||
|
||||
private struct PullOffsetPosition: Equatable {
|
||||
let type: PullOffsetPositionType
|
||||
let minY: CGFloat
|
||||
}
|
||||
|
||||
private struct PullOffsetPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: [PullOffsetPosition] = []
|
||||
|
||||
static func reduce(value: inout [PullOffsetPosition], nextValue: () -> [PullOffsetPosition]) {
|
||||
value.append(contentsOf: nextValue())
|
||||
}
|
||||
}
|
||||
|
||||
private struct PullOffsetPositionReader: View {
|
||||
let type: PullOffsetPositionType
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
Color.clear.preference(
|
||||
key: PullOffsetPreferenceKey.self,
|
||||
value: [PullOffsetPosition(type: type, minY: proxy.frame(in: .global).minY)]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,9 @@ struct MainChatView: View {
|
||||
} else if viewModel.shouldShowEmptyState {
|
||||
emptyContent
|
||||
} else {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(viewModel.rooms) { room in
|
||||
MainChatRoomListItem(room: room) { roomId, chatType in
|
||||
@@ -79,39 +81,34 @@ struct MainChatView: View {
|
||||
}
|
||||
.padding(.top, 0)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadingContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.tint(Color.soda400)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
Text(I18n.MainChat.emptyStateMessage)
|
||||
.appFont(size: 16, weight: .medium)
|
||||
.foregroundColor(Color.gray500)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,9 @@ struct MainContentAllView: View {
|
||||
} else if viewModel.shouldShowEmptyState {
|
||||
emptyContent
|
||||
} else {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
gridContent
|
||||
.padding(.horizontal, SodaSpacing.s14)
|
||||
.padding(.top, SodaSpacing.s8)
|
||||
@@ -86,36 +88,31 @@ struct MainContentAllView: View {
|
||||
.padding(.vertical, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadingContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
MainContentAudioEmptyStateView(message: I18n.MainContentAll.emptyStateMessage)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,20 +40,21 @@ struct MainContentRankingView: View {
|
||||
|
||||
private var loadingContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var rankingContent: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||
if let topItem {
|
||||
MainContentAudioRankingTopCard(
|
||||
@@ -104,20 +105,16 @@ struct MainContentRankingView: View {
|
||||
.padding(.horizontal, SodaSpacing.s8)
|
||||
.padding(.vertical, SodaSpacing.s20)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
MainContentAudioRankingEmptyStateView(message: viewModel.emptyStateMessage)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ struct MainContentRecommendationView: View {
|
||||
} else if viewModel.shouldShowEmptyState {
|
||||
emptyContent
|
||||
} else {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s24) {
|
||||
if let recommendations = viewModel.recommendations {
|
||||
MainContentAudioBannerSection(
|
||||
@@ -80,9 +82,6 @@ struct MainContentRecommendationView: View {
|
||||
}
|
||||
.padding(.vertical, SodaSpacing.s20)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -99,27 +98,25 @@ struct MainContentRecommendationView: View {
|
||||
|
||||
private var loadingContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
MainContentAudioEmptyStateView(message: viewModel.emptyStateMessage)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,9 @@ struct MainHomeFollowingView: View {
|
||||
} else if !viewModel.message.isEmpty {
|
||||
emptyContent(message: viewModel.message)
|
||||
} else if let response = viewModel.response {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
VStack(spacing: 0) {
|
||||
if !response.followingCreators.isEmpty {
|
||||
MainHomeFollowingCreatorSection(
|
||||
@@ -104,9 +106,6 @@ struct MainHomeFollowingView: View {
|
||||
.padding(.bottom, SodaSpacing.s20)
|
||||
}
|
||||
.background(Color.black)
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
} else {
|
||||
emptyContent(message: I18n.HomeFollowing.emptyStateMessage)
|
||||
}
|
||||
@@ -114,31 +113,29 @@ struct MainHomeFollowingView: View {
|
||||
|
||||
private var loadingContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
ProgressView()
|
||||
.tint(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.background(Color.black)
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func emptyContent(message: String, loginAction: (() -> Void)? = nil) -> some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
MainHomeFollowingEmptyStateView(
|
||||
message: message,
|
||||
loginAction: loginAction
|
||||
)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,20 +31,21 @@ struct MainHomeRankingView: View {
|
||||
|
||||
private var loadingContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var rankingContent: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||
ForEach(firstRankItems) { item in
|
||||
MainHomeRankingCard(
|
||||
@@ -96,20 +97,16 @@ struct MainHomeRankingView: View {
|
||||
.padding(.horizontal, SodaSpacing.s8)
|
||||
.padding(.vertical, SodaSpacing.s20)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
MainHomeRankingEmptyStateView()
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ struct MainHomeRecommendationView: View {
|
||||
if viewModel.isLoading && viewModel.recommendations == nil && viewModel.errorMessage.isEmpty {
|
||||
loadingContent
|
||||
} else {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s24) {
|
||||
MainHomeLiveSection(
|
||||
items: viewModel.recommendations?.lives ?? [],
|
||||
@@ -82,9 +84,6 @@ struct MainHomeRecommendationView: View {
|
||||
}
|
||||
.padding(.vertical, SodaSpacing.s20)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -119,15 +118,14 @@ struct MainHomeRecommendationView: View {
|
||||
|
||||
private var loadingContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LottieRefreshableScrollView(action: {
|
||||
await viewModel.refresh()
|
||||
}) {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
| 문서 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | 구현 완료 |
|
||||
| 상태 | Phase 4 layout 보존 motion 자동 검증 완료 / 사용자 확인 대기 |
|
||||
| 작성일 | `2026-08-18` |
|
||||
| 요구사항 기준 | `docs/20260818_메인_탭_당겨서_새로고침/prd.md` |
|
||||
| API 기준 | 기존 홈/콘텐츠/대화 API 계약 재사용, 신규 API 없음 |
|
||||
| 현재 Phase | Phase 1~3 완료 |
|
||||
| 현재 활성 Goal | 없음 |
|
||||
| 현재 Phase | Phase 1~3 완료, Phase 4 Gate 수동 검증 대기 |
|
||||
| 현재 활성 Goal | `P4-R3` |
|
||||
|
||||
## 목표
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
| 1 | 완료 | `1/1` | 없음 | 없음 |
|
||||
| 2 | 완료 | `1/1` | 없음 | 없음 |
|
||||
| 3 | 완료 | `1/1` | 없음 | 없음 |
|
||||
| 4 | 회귀 수정 대기 | `4/5` | `P4-R3` | layout 보존 motion 구현·검증 |
|
||||
|
||||
- 동시에 하나의 미완료 goal만 운용한다.
|
||||
- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다. 후속 수정은 회귀 수정 Task와 새 goal ID를 추가한다.
|
||||
@@ -34,6 +35,9 @@
|
||||
- 콘텐츠 `랭킹`의 현재 `AudioRankingType`, 콘텐츠 `전체`의 현재 `MainContentAllType`, `ContentSort`, `SeriesPublishedDaysOfWeek` 유지.
|
||||
- 대화 탭의 현재 `MainChatFilter` 유지와 첫 페이지 재조회.
|
||||
- loading, empty, error, content 상태에서 모두 새로고침 가능하도록 상태 화면 구조 점검.
|
||||
- 메인 홈·콘텐츠·대화 7개 View의 20개 refresh 가능 상태에서 기본 indicator를 `pull-to-refresh.json` Lottie로 교체.
|
||||
- Lottie를 최대 `48×48pt`, 원본 비율 유지, 중앙 정렬하고 상하 `8pt`를 포함한 총 `64pt` refresh 영역으로 표시.
|
||||
- Reduce Motion 활성화 시 animation을 재생하지 않고 첫 번째 유효 frame `1`을 표시.
|
||||
|
||||
### 제외
|
||||
|
||||
@@ -41,14 +45,18 @@
|
||||
- 하단 탭 전체 일괄 새로고침.
|
||||
- 신규 API endpoint, 신규 dependency, 신규 공통 refresh abstraction.
|
||||
- 자동 갱신, polling, push 기반 갱신.
|
||||
- `LiveNowAllView`, `ContentDetailView`와 그 밖의 legacy 화면 pull-to-refresh 변경.
|
||||
|
||||
## 기술적 제약
|
||||
|
||||
- 기술 스택: Swift, SwiftUI, Combine, Moya, CocoaPods 기반 iOS 앱.
|
||||
- 아키텍처: 기존 `View -> ViewModel -> Repository -> Api(TargetType)` 흐름을 유지한다.
|
||||
- UI: SwiftUI `.refreshable` 또는 동등한 iOS 기본 refresh affordance를 우선 사용한다.
|
||||
- 후속 UI: SwiftUI `.refreshable`은 custom indicator API를 제공하지 않으므로 Phase 4에서는 V2 공용 `LottieRefreshableScrollView` 한 개로만 대체한다.
|
||||
- Animation: Lottie `4.6.1`, `SodaLive/Resources/pull-to-refresh.json`만 사용한다. asset은 `150×150`, `30fps`, frame `0..<45`, marker 없음이다.
|
||||
- 접근성: `accessibilityReduceMotion == true`이면 frame `1`을 정지 표시하고, Lottie visual은 접근성 트리에서 숨긴다. refresh action 이름은 기존 `I18n.LiveNow.refreshButton`을 재사용한다.
|
||||
- 데이터·보안: 기존 인증 header, token 저장, 언어 header 흐름을 변경하지 않는다.
|
||||
- 의존성: 신규 dependency를 추가하지 않는다.
|
||||
- 의존성: Lottie `4.6.1` 외 신규 dependency를 추가하지 않고, 기존 `RefreshableScrollView` package source는 수정하지 않는다.
|
||||
- 계약: 제공되지 않은 endpoint, DTO, enum, 오류 status/key를 추정하지 않는다.
|
||||
- 구현: 모든 구현 Task는 `RED → GREEN → REFACTOR` 순서를 체크박스에 명시한다. 현재 저장소는 테스트 번들 타깃이 명확하지 않으므로 테스트 작성이 불가능한 Task는 대체 검증 방법을 사용한다.
|
||||
- 검증: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`와 수동 QA를 필수 Gate로 둔다.
|
||||
@@ -267,6 +275,178 @@ xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration D
|
||||
- [x] 빈/오류 상태에서도 새로고침을 재시도할 수 있는지 확인한다.
|
||||
- [x] 새로고침 후 스크롤 하단에서 다음 페이지 조회가 이어지는지 확인한다.
|
||||
|
||||
## Phase 4 Lottie 새로고침 표시
|
||||
|
||||
**Phase 결과:** 메인 홈·콘텐츠·대화 7개 View의 모든 refresh 가능 상태에서 기본 indicator 대신 지정된 Lottie가 표시되고 기존 새로고침 동작은 유지된다.
|
||||
|
||||
**선행조건:** Phase 1~3 Gate 완료, PRD `REFRESH-ANIMATION-001~005`와 `DEC-004~011` 확정, 사용자 코드 구현 승인.
|
||||
|
||||
**Phase 완료 조건:** `P4-T1`, `P4-T2`, `P4-GATE` 완료와 검증 기록 누적.
|
||||
|
||||
### 구현 항목
|
||||
|
||||
#### Task 4.1 공용 Lottie refresh component
|
||||
|
||||
**Goal 실행 `P4-T1`:** Lottie 표시와 pull-to-refresh 상태를 한 곳에서 관리하는 V2 공용 vertical scroll component를 만든다.
|
||||
|
||||
- **시작 조건:** `SodaLive/Resources/pull-to-refresh.json`과 Lottie `4.6.1`이 `SodaLive`, `SodaLive-dev` target에 포함되어 있다.
|
||||
- **완료 증거:** component source 2개가 두 target에서 빌드되고, pull offset·단일 refresh·완료 초기화·Reduce Motion 상태가 아래 contract를 따른다.
|
||||
- **범위 밖:** 메인 7개 View 연결, legacy `RefreshableScrollView` 변경, pull progress와 animation progress 연동.
|
||||
- **TDD 예외 사유:** 현재 저장소에 앱 test bundle target이 없고, ScrollView bounce와 Lottie playback은 runtime UI 동작이다.
|
||||
- **대체 검증 방법:** asset metadata 검사, 두 scheme Debug 빌드, Phase 4 수동 상태 전이 확인.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshIndicator.swift`
|
||||
- Create: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshableScrollView.swift`
|
||||
- Modify: `SodaLive.xcodeproj/project.pbxproj` — 두 source를 `SodaLive`, `SodaLive-dev` Sources build phase에 추가
|
||||
- Verify: `SodaLive/Resources/pull-to-refresh.json`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```swift
|
||||
struct LottieRefreshIndicator: View {
|
||||
let isAnimating: Bool
|
||||
}
|
||||
|
||||
struct LottieRefreshableScrollView<Content: View>: View {
|
||||
init(
|
||||
action: @escaping () async -> Void,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- Consumes: Lottie `LottieView`, `pull-to-refresh.json`, `SodaSpacing.s8`, `SodaSpacing.s48`, `EnvironmentValues.accessibilityReduceMotion`, `I18n.LiveNow.refreshButton`.
|
||||
- Produces: indicator가 없는 vertical `ScrollView`, `64pt` trigger/표시 영역, 중복 실행을 막는 단일 async refresh action, accessibility refresh action.
|
||||
|
||||
- [x] **Asset 확인:** JSON이 `150×150`, `30fps`, frame `0..<45`이고 frame `0`은 비어 있으며 frame `1`부터 stroke가 표시되는지 확인한다.
|
||||
- [x] **GREEN:** `LottieRefreshIndicator`는 일반 설정에서 영역 노출 중 frame `0..<45`를 반복 재생하고, Reduce Motion에서는 frame `1`에 정지한다. 크기는 aspect fit 최대 `48×48pt`, 중앙 정렬, Lottie 자체는 accessibility tree에서 숨긴다.
|
||||
- [x] **GREEN:** `LottieRefreshableScrollView`는 양수 pull offset에서 indicator를 보이고, `64pt` 기준으로 action을 한 번만 실행하며, action이 끝날 때까지 영역을 유지한 뒤 offset·animation을 처음 상태로 되돌린다.
|
||||
- [x] **GREEN:** 기준 거리 미만에서 손을 놓으면 API를 호출하지 않고 indicator를 숨겨 frame `0`으로 초기화하며, refresh 중 재당김은 두 번째 action을 만들지 않는다.
|
||||
- [x] **GREEN 확인:** 두 scheme을 빌드해 Lottie API, async closure, project source membership 오류가 없음을 확인한다.
|
||||
- [x] **REFACTOR:** offset 측정용 `PreferenceKey`와 refresh 상태는 `LottieRefreshableScrollView.swift` 내부 private 구현으로 두고 추가 protocol·ViewModel·config type을 만들지 않는다.
|
||||
|
||||
#### Task 4.2 메인 7개 View 연결
|
||||
|
||||
**Goal 실행 `P4-T2`:** 기존 7개 View의 20개 `.refreshable` 경로를 공용 Lottie refresh component로 교체한다.
|
||||
|
||||
- **시작 조건:** `P4-T1` 완료.
|
||||
- **완료 증거:** 대상 파일의 `.refreshable` 0건, 기존 `refresh()` 호출·상태 분기·레이아웃 유지, 두 scheme 빌드와 수동 QA 통과.
|
||||
- **범위 밖:** 7개 ViewModel 변경, `LiveNowAllView`, `ContentDetailView`, 최초 loading용 본문 `ProgressView` 디자인 변경.
|
||||
- **TDD 예외 사유:** 현재 앱 test bundle target이 없는 SwiftUI 화면 연결 변경이다.
|
||||
- **대체 검증 방법:** 대상 modifier 정적 검색, ViewModel diff 부재 확인, 두 scheme Debug 빌드, 화면별 수동 QA.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift`
|
||||
- Modify: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`
|
||||
- Modify: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift`
|
||||
- Modify: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift`
|
||||
- Modify: `SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift`
|
||||
- Modify: `SodaLive/Sources/V2/Main/Content/All/MainContentAllView.swift`
|
||||
- Modify: `SodaLive/Sources/V2/Main/Chat/MainChatView.swift`
|
||||
- Test: 별도 test file 없음 — Phase 4 Gate의 정적 검사·빌드·수동 검증 사용
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `LottieRefreshableScrollView(action:content:)`와 기존 7개 ViewModel의 `refresh() async`.
|
||||
- Produces: content·loading·empty/error 분기 전체에서 동일한 Lottie pull-to-refresh 사용자 동작.
|
||||
|
||||
- [x] **상태 확인:** 현재 7개 View의 `.refreshable` 20곳과 각 content·loading·empty/error 분기를 대조한다.
|
||||
- [x] **GREEN:** 각 `ScrollView`와 `.refreshable` 조합을 `LottieRefreshableScrollView`로 교체하고 action에는 기존 `await viewModel.refresh()`만 연결한다.
|
||||
- [x] **GREEN:** 기존 `frame(minHeight:)`, background, padding, pagination `onAppear`와 최초 loading `ProgressView`를 그대로 유지한다.
|
||||
- [x] **GREEN 확인:** 대상 7개 파일의 `.refreshable`이 0건이고 `LottieRefreshableScrollView` 적용 수가 기존 상태 분기 수와 일치하는지 정적 검사한다.
|
||||
- [ ] **GREEN 확인:** 홈·콘텐츠·대화의 선택 탭·필터·pagination·실패 재시도 동작이 Phase 1~3 결과와 동일한지 수동 확인한다.
|
||||
- [x] **REFACTOR:** 7개 ViewModel, legacy pull-to-refresh 화면과 요청 범위 밖 UI에 diff가 없는지 확인한다.
|
||||
|
||||
#### Task 4.3 pull trigger 회귀 수정
|
||||
|
||||
**Goal 실행 `P4-R1`:** Phase 4 첫 수동 검증에서 Lottie 표시와 refresh가 모두 실행되지 않은 회귀를 수정한다.
|
||||
|
||||
- **원인:** viewport와 같은 짧은 content의 bounce 부재, 신뢰할 수 없는 `DragGesture.onEnded`, 현재 hierarchy에서 양수 pull offset을 만들지 못한 named coordinate 측정이 겹쳤다.
|
||||
- **TDD 예외 사유:** 앱 test bundle target이 없고 실제 `ScrollView` rubber-band와 gesture arbitration이 필요한 runtime UI 회귀다.
|
||||
- **대체 검증 방법:** 사용자 실패 재현, Apple·OSS·설치 package source 비교, 정적 검사, 두 scheme Debug 빌드, 사용자 재확인.
|
||||
- **Files:**
|
||||
- Modify: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshableScrollView.swift`
|
||||
|
||||
- [x] **RED:** 사용자가 당김 시 Lottie 표시와 refresh가 모두 실행되지 않음을 확인했다.
|
||||
- [x] **원인 확인:** 짧은 content의 bounce 부재, `DragGesture.onEnded` 트리거와 offset 측정을 기존 동작 package·Apple·OSS 패턴과 비교했다.
|
||||
- [x] **GREEN:** vertical bounce를 항상 허용하고 global moving-minus-fixed offset이 `64pt`를 넘겼다가 돌아오는 전이에서 action을 한 번만 실행하도록 수정했다.
|
||||
- [x] **GREEN 확인:** named coordinate·`DragGesture` 0건, global moving/fixed 측정과 bounce·armed 전이 존재, 두 scheme Debug build 성공을 확인했다.
|
||||
- [x] **수동 확인:** 기존 실패 화면에서 Lottie가 표시되고 refresh action이 한 번 실행되는지 사용자가 재확인한다.
|
||||
|
||||
#### Task 4.4 refresh motion 회귀 수정 (롤백)
|
||||
|
||||
**Goal 실행 `P4-R2`:** 기능 복구 후 refresh 시작·완료 시 발생하는 UI 덜컹임을 제거한다.
|
||||
|
||||
- **원인:** ScrollView bounce-back 중 refresh row 높이 `0→64pt`와 indicator offset `-64→0`이 동시에 즉시 변경되고, 완료 시 row가 `64→0pt`로 즉시 축소됐다.
|
||||
- **TDD 예외 사유:** 앱 test bundle target이 없고 실제 ScrollView rubber-band와 layout handoff가 필요한 runtime motion 회귀다.
|
||||
- **대체 검증 방법:** 사용자 체감 RED, 기존 package freeze 패턴 비교, 정적 검사, 두 scheme Debug 빌드, 사용자 재확인.
|
||||
- **Files:**
|
||||
- Modify: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshableScrollView.swift`
|
||||
|
||||
- [x] **RED:** 사용자가 Lottie 표시와 refresh는 동작하지만 진행이 덜컹거림을 확인했다.
|
||||
- [x] **원인 확인:** 시작·완료의 구조적 row 삽입/축소와 indicator 위치 전환이 bounce-back과 충돌함을 확인했다.
|
||||
- [x] **GREEN:** 구조적 refresh row를 제거하고 content·indicator가 같은 frozen alignment handoff를 사용하도록 수정했다.
|
||||
- [x] **GREEN 확인:** frozen alignment, Reduce Motion 분기, 완료 animation과 두 scheme Debug build 성공을 확인했다.
|
||||
- [x] **수동 확인:** motion은 부드러워졌지만 기존 UI 간격이 변경되는 회귀를 사용자가 확인했다.
|
||||
- [x] **ROLLBACK:** 사용자 결정에 따라 frozen motion handoff와 후속 layout 보정을 모두 제거하고 `P4-R1` 기능 복구 상태로 되돌렸다.
|
||||
|
||||
#### Task 4.5 layout 보존 refresh motion 수정
|
||||
|
||||
**Goal 실행 `P4-R3`:** 기존 `VStack` layout과 UI 간격을 유지하면서 refresh 시작·완료의 목록 위치 전환을 부드럽게 한다.
|
||||
|
||||
- **원인:** `isRefreshing` 전환 때 refresh 영역 높이와 indicator offset이 animation 없이 즉시 `0↔64pt`로 변경되어 목록이 위아래로 순간 이동한다.
|
||||
- **선택 설계:** 현재 `VStack`과 `refreshRegion` 구조는 유지하고 `isRefreshing` 시작·완료 상태만 동일한 `.easeOut(duration: 0.2)` transaction으로 처리한다.
|
||||
- **대안 제외:** 고정 64pt 영역은 scroll layout을 변경하고, `UIRefreshControl` 재작성은 현재 회귀에 비해 범위가 크므로 제외한다.
|
||||
- **접근성:** Reduce Motion에서는 animation 없이 즉시 상태를 전환한다.
|
||||
- **TDD 예외 사유:** 앱 test bundle target이 없고 실제 ScrollView rubber-band와 layout animation이 필요한 runtime motion 회귀다.
|
||||
- **대체 검증 방법:** 사용자 체감 RED, 구조·상태 전이 정적 검사, 두 scheme Debug 빌드, 사용자 재확인.
|
||||
- **Files:**
|
||||
- Modify: `SodaLive/Sources/V2/Component/Refresh/LottieRefreshableScrollView.swift`
|
||||
|
||||
- [x] **RED:** Lottie와 refresh는 동작하지만 시작·완료 때 목록이 위아래로 끊겨 이동함을 사용자가 확인했다.
|
||||
- [x] **설계 승인:** 기존 구조의 높이·offset 전환만 animate하는 최소안을 사용자가 선택했다.
|
||||
- [x] **GREEN:** refresh 시작·완료를 같은 Reduce Motion-aware animation transaction으로 처리한다.
|
||||
- [x] **GREEN 확인:** 기존 `VStack`, global offset 측정, bounce, armed trigger와 두 scheme build 성공을 확인한다.
|
||||
- [ ] **수동 확인:** 기존 UI 간격이 유지되고 시작·완료 목록 이동이 부드러운지 사용자가 확인한다.
|
||||
|
||||
### 완료 조건
|
||||
|
||||
- [ ] `P4-T1`, `P4-T2`의 체크박스와 완료 증거가 모두 충족됐다.
|
||||
- [x] `REFRESH-ANIMATION-001~005`가 구현 또는 명시적 제외로 추적된다.
|
||||
- [x] Lottie와 기본 indicator가 동시에 보이는 대상 상태가 없다.
|
||||
- [ ] Phase 1~3에서 검증한 데이터 refresh 동작에 회귀가 없다.
|
||||
|
||||
### 검증 방법
|
||||
|
||||
#### Phase 4 Gate
|
||||
|
||||
**Goal 실행 `P4-GATE`:** Lottie 표시, 접근성 설정, 기존 데이터 refresh와 범위 제외를 최종 판정한다.
|
||||
|
||||
- **시작 조건:** `P4-T1`, `P4-T2` 완료.
|
||||
- **완료 증거:** 아래 자동·수동 검증 통과와 Progress 기록.
|
||||
- **범위 밖:** Gate 실패와 무관한 UI 변경, legacy refresh 화면 변경, test 삭제·완화.
|
||||
|
||||
```bash
|
||||
jq -e '.w == 150 and .h == 150 and .fr == 30 and .ip == 0 and .op == 45' SodaLive/Resources/pull-to-refresh.json
|
||||
! rg -n '\.refreshable' SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift SodaLive/Sources/V2/Main/Content/All/MainContentAllView.swift SodaLive/Sources/V2/Main/Chat/MainChatView.swift
|
||||
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build CODE_SIGNING_ALLOWED=NO
|
||||
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build CODE_SIGNING_ALLOWED=NO
|
||||
git diff --check
|
||||
```
|
||||
|
||||
**Expected:** 모든 명령 exit code 0. 대상 7개 파일에 `.refreshable`이 없고 두 scheme이 빌드되며, asset metadata와 target membership이 일치한다.
|
||||
|
||||
수동 검증:
|
||||
|
||||
- [ ] 기준 거리 미만으로 당기는 동안 Lottie가 반복 재생되고 손을 놓으면 API 호출 없이 사라진 뒤 다음 당김에서 처음부터 재생된다.
|
||||
- [ ] `64pt` 기준을 넘겨 새로고침하면 Lottie가 API 완료까지 유지되고 기본 indicator는 보이지 않으며 요청은 한 번만 실행된다.
|
||||
- [ ] Reduce Motion을 켜면 frame `1`이 정지 표시되고, accessibility refresh action으로도 동일한 기존 `refresh()`가 한 번 실행된다.
|
||||
- [ ] 홈 3개·콘텐츠 3개·대화 View의 content·loading·empty/error 상태에서 최대 `48×48pt` Lottie와 기존 데이터 갱신 동작을 확인한다.
|
||||
- [ ] iPhone과 iPad에서 영역이 총 `64pt`이고 content가 겹치거나 잘리지 않으며, `LiveNowAllView`와 `ContentDetailView`는 기존 표시를 유지한다.
|
||||
|
||||
## 실행 순서와 의존성
|
||||
|
||||
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||
@@ -277,16 +457,19 @@ xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration D
|
||||
| 4 | `P2-GATE` | `P2-T1` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
|
||||
| 5 | `P3-T1` | `P2-GATE` | 아니요 | 대화 탭 상태 분기 재확인 |
|
||||
| 6 | `P3-GATE` | `P3-T1` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
|
||||
| 7 | `P4-T1` | `P3-GATE`, 구현 승인 | 아니요 | asset·Lottie target membership 재확인 |
|
||||
| 8 | `P4-T2` | `P4-T1` | 아니요 | 대상 View의 상태 분기와 wrapper contract 재확인 |
|
||||
| 9 | `P4-GATE` | `P4-T2` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
|
||||
|
||||
```text
|
||||
P1-T1 -> P1-GATE -> P2-T1 -> P2-GATE -> P3-T1 -> P3-GATE
|
||||
P1-T1 -> P1-GATE -> P2-T1 -> P2-GATE -> P3-T1 -> P3-GATE -> P4-T1 -> P4-T2 -> P4-GATE
|
||||
```
|
||||
|
||||
## 변경 금지 항목
|
||||
|
||||
- 확정된 요구사항을 근거 없이 변경하지 않는다.
|
||||
- 기존 완료 체크박스와 Progress·Decision Log·검증 기록을 삭제하거나 덮어쓰지 않는다.
|
||||
- 신규 endpoint, DTO, dependency, 공통 refresh abstraction을 만들지 않는다.
|
||||
- 신규 endpoint, DTO, dependency와 `P4-T1`의 component 2개 외 추가 refresh abstraction을 만들지 않는다.
|
||||
- 요청 범위 밖의 리팩터링과 UI 재설계를 하지 않는다.
|
||||
- test를 삭제·skip·완화하거나 타입 오류를 우회해 Gate를 통과시키지 않는다.
|
||||
- JWT, password, signed URL, 개인 메시지 본문 같은 민감정보를 log·fixture·문서에 기록하지 않는다.
|
||||
@@ -355,13 +538,89 @@ P1-T1 -> P1-GATE -> P2-T1 -> P2-GATE -> P3-T1 -> P3-GATE
|
||||
- 남은 항목: 없음.
|
||||
- 다음 행동: 없음.
|
||||
|
||||
### Phase 4 요구사항 인터뷰와 계획 작성 — 2026-08-25
|
||||
|
||||
- 상태: 계획 완료 / 구현 미착수
|
||||
- 무엇을: Lottie playback·크기·Reduce Motion·적용 범위를 확정하고 `P4-T1`, `P4-T2`, `P4-GATE`를 작성했다.
|
||||
- 왜: 기존 7개 View·20개 상태에 같은 표시 규칙을 적용하되 legacy 화면과 데이터 refresh 로직을 변경하지 않기 위해 작성했다.
|
||||
- 어떻게:
|
||||
- `rg`로 `.refreshable`과 `RefreshableScrollView` 실제 사용 화면 확인 — 메인 7개 View와 legacy 2개 View 확인.
|
||||
- `jq`로 `pull-to-refresh.json` 확인 — `150×150`, `30fps`, frame `0..<45`, frame `0` 비어 있음 확인.
|
||||
- Xcode project와 `Package.resolved` 확인 — Lottie `4.6.1` 및 asset이 `SodaLive`, `SodaLive-dev` 두 target에 포함됨 확인.
|
||||
- Lottie·Apple 공식 문서와 현재 `RefreshableScrollView` `1.1.1` source 확인 — 공용 V2 component 계획 근거 확인.
|
||||
- `git diff --check -- docs/20260818_메인_탭_당겨서_새로고침/prd.md docs/20260818_메인_탭_당겨서_새로고침/plan-task.md` — 성공, exit code 0.
|
||||
- placeholder·미결정 검색 — 남은 placeholder와 미결 요구사항 없음.
|
||||
- `git diff --name-only -- SodaLive/Sources` — 출력 없음, 앱 source 코드 미수정 확인.
|
||||
- 남은 항목: `P4-T1`, `P4-T2`, `P4-GATE` 전체. 이번 요청에서는 코드 구현하지 않음.
|
||||
- 다음 행동: 사용자가 별도로 코드 구현을 요청하면 `P4-T1`부터 시작.
|
||||
|
||||
### `P4-T1`~`P4-T2` 구현과 자동 Gate — 2026-08-25
|
||||
|
||||
- 상태: 구현·자동 검증 완료 / 사용자 수동 검증 대기
|
||||
- 무엇을: V2 공용 Lottie refresh component 2개를 만들고 메인 7개 View의 `.refreshable` 20곳을 교체했다.
|
||||
- 왜: 기존 데이터 새로고침 동작을 유지하면서 지정된 Lottie 표시·접근성·중복 요청 방지 규칙을 한 곳에서 적용하기 위해 구현했다.
|
||||
- 어떻게:
|
||||
- `jq` asset metadata 검사 — `150×150`, `30fps`, frame `0..<45` 확인, exit code 0.
|
||||
- 정적 검사 — 대상 7개 View의 `.refreshable` 0건, `LottieRefreshableScrollView` 20건(`2/3/3/3/3/3/3`) 확인.
|
||||
- 범위 검사 — 7개 ViewModel, `LiveNowAllView`, `ContentDetailView` diff 없음 확인.
|
||||
- `xcodebuild` `SodaLive` Debug simulator build — 성공, exit code 0, `BUILD SUCCEEDED`.
|
||||
- `xcodebuild` `SodaLive-dev` Debug simulator build — 성공, exit code 0, `BUILD SUCCEEDED`.
|
||||
- `git diff --check` — 성공, 오류 없음.
|
||||
- P4-T1·P4-T2 spec review와 code-quality review — 차단 finding 없이 승인.
|
||||
- visual-qa — 사용자 요청에 따라 미실행.
|
||||
- 남은 항목: Phase 4 수동 검증 5개와 `P4-GATE` 최종 완료 처리.
|
||||
- 다음 행동: 사용자가 아래 Phase 4 Lottie 후속 검증 5개를 확인하고 결과를 전달한다.
|
||||
|
||||
### `P4-R1` pull trigger 회귀 수정 — 2026-08-25
|
||||
|
||||
- 상태: 수정·자동 검증 완료 / 사용자 재확인 대기
|
||||
- 무엇을: `LottieRefreshableScrollView`의 짧은 content bounce와 refresh release 판정을 수정했다.
|
||||
- 왜: Phase 4 첫 수동 검증에서 Lottie 표시와 데이터 refresh가 모두 실행되지 않았기 때문이다.
|
||||
- 어떻게:
|
||||
- 사용자 RED — 당김 시 Lottie 표시 없음, refresh 실행 없음 확인.
|
||||
- 원인 조사 — `ScrollView` content-size 기반 bounce와 custom `DragGesture.onEnded` 취소 가능성을 Apple·OSS·기존 package source로 교차 확인.
|
||||
- 1차 수정 — `.scrollBounceBehavior(.always, axes: .vertical)` 추가, `DragGesture` 제거, preference armed→release 전이 적용. 사용자 재확인에서 동일 증상 확인.
|
||||
- 2차 수정 — 기존 동작 package와 같은 global moving-minus-fixed frame 차이로 pull offset 측정 교체.
|
||||
- 정적 검사 — named coordinate·`DragGesture` 0건, global moving/fixed 측정과 bounce·armed 상태 전이 확인.
|
||||
- `xcodebuild` `SodaLive`, `SodaLive-dev` Debug simulator build — 성공, `BUILD SUCCEEDED`.
|
||||
- 남은 항목: 기존 실패 화면 사용자 재확인과 Phase 4 수동 검증 5개.
|
||||
- 다음 행동: 사용자가 기존 실패 화면에서 다시 당겨 Lottie 표시와 refresh 실행 여부를 확인한다.
|
||||
|
||||
### `P4-R2` motion 보정과 롤백 — 2026-08-25
|
||||
|
||||
- 상태: 사용자 결정으로 롤백
|
||||
- 무엇을: refresh 시작·완료 덜컹임을 줄이기 위해 frozen alignment handoff를 적용했으나 기존 UI 간격이 변경되어 해당 보정과 후속 layout 수정을 모두 제거했다.
|
||||
- 왜: 사용자 우선순위에 따라 기존 UI layout을 motion 개선보다 우선 보존하기 위해 롤백했다.
|
||||
- 어떻게:
|
||||
- motion 보정 후 새로고침 진행이 부드러워짐을 사용자 확인.
|
||||
- 기존 UI 간격 변경 회귀를 사용자 확인.
|
||||
- `LottieRefreshableScrollView`를 global moving-minus-fixed offset 기반 `P4-R1` 기능 복구 상태로 복원.
|
||||
- 남은 항목: Phase 4 수동 검증 잔여 항목.
|
||||
- 다음 행동: 기존 UI layout과 Lottie refresh 기능 유지 여부를 확인한다.
|
||||
|
||||
### `P4-R3` layout 보존 motion 수정 — 2026-08-26
|
||||
|
||||
- 상태: 구현·자동 검증 완료 / 사용자 확인 대기
|
||||
- 무엇을: 기존 `VStack`과 refresh hierarchy를 유지한 채 시작·완료의 `isRefreshing` 상태 전환만 `.easeOut(duration: 0.2)`로 처리했다.
|
||||
- 왜: Lottie와 refresh 기능은 정상이지만 refresh 영역 높이가 즉시 `0↔64pt`로 바뀌어 목록이 위아래로 순간 이동했기 때문이다.
|
||||
- 어떻게:
|
||||
- 사용자 RED — Lottie와 refresh는 동작하지만 목록 위치가 시작·완료 때 끊겨 이동함을 확인.
|
||||
- 정적 검사 — `VStack(spacing: 0)`, global moving-minus-fixed 측정, bounce, armed trigger, refresh 영역 높이·offset 식 보존 확인.
|
||||
- 정적 검사 — Reduce Motion-aware `withAnimation` 2건, `ZStack`, `alignmentGuide`, `UIRefreshControl`, `frozen` 0건 확인.
|
||||
- `git diff --check` — 성공, 오류 없음.
|
||||
- `xcodebuild` `SodaLive`, `SodaLive-dev` Debug simulator build — 각각 성공, `BUILD SUCCEEDED` 2건 확인.
|
||||
- spec compliance review와 code-quality review — 승인, runtime motion과 layout은 사용자 확인 대상으로 유지.
|
||||
- TDD 예외: 앱 test bundle target이 없고 ScrollView rubber-band와 layout animation의 runtime 상호작용이 필요한 UI 회귀이므로 사용자 RED와 정적 검사·두 scheme build를 대체 증거로 사용했다.
|
||||
- 남은 항목: 기존 UI 간격 유지, 시작·완료 목록 이동, Reduce Motion, 단일 refresh 실행 사용자 확인.
|
||||
- 다음 행동: 사용자가 기존 화면에서 refresh를 다시 실행해 layout과 motion을 확인한다.
|
||||
|
||||
## 사용자 수동 테스트 목록
|
||||
|
||||
사전 조건:
|
||||
|
||||
- Debug 앱을 실제 기기 또는 iOS Simulator에서 실행한다.
|
||||
- 로그인 후 메인 화면에 진입한다.
|
||||
- 각 테스트에서 화면 상단 목록을 아래로 충분히 당겨 iOS 기본 새로고침 indicator가 보이는지 확인한다.
|
||||
- 각 테스트에서 화면 상단 목록을 아래로 충분히 당겨 Lottie 새로고침 표시가 보이는지 확인한다.
|
||||
|
||||
### 홈 탭
|
||||
|
||||
@@ -392,6 +651,14 @@ P1-T1 -> P1-GATE -> P2-T1 -> P2-GATE -> P3-T1 -> P3-GATE
|
||||
- [x] 새로고침 실패 시 기존 토스트 또는 오류/빈 상태가 표시되고 다시 당겨 재시도할 수 있다.
|
||||
- [x] 하단 `마이` 탭에는 이번 변경으로 새로고침 동작이 추가되지 않았다.
|
||||
|
||||
### Phase 4 Lottie 후속 검증
|
||||
|
||||
- [ ] 당김 영역 노출 중 반복 재생, 기준 미만 취소 시 숨김·초기화를 확인한다.
|
||||
- [ ] refresh 시작 후 API 완료까지 Lottie가 유지되고 요청이 한 번만 실행되는지 확인한다.
|
||||
- [ ] Reduce Motion에서 frame `1` 정지 표시와 accessibility refresh action을 확인한다.
|
||||
- [ ] 홈·콘텐츠·대화 7개 View의 content·loading·empty/error 상태와 iPhone·iPad 레이아웃을 확인한다.
|
||||
- [ ] legacy `LiveNowAllView`, `ContentDetailView`가 변경되지 않았는지 확인한다.
|
||||
|
||||
## Decision Log
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||
@@ -399,23 +666,28 @@ P1-T1 -> P1-GATE -> P2-T1 -> P2-GATE -> P3-T1 -> P3-GATE
|
||||
| 2026-08-18 | `DEC-001` | 확정 | 당겨서 새로고침은 현재 선택된 내부 탭·필터·정렬 상태만 대상으로 한다. | 사용자 A안 선택 | `P1-T1`, `P2-T1`, `P3-T1`, `prd.md` |
|
||||
| 2026-08-18 | `DEC-002` | 확정 | 빈/오류 상태에서도 당겨서 새로고침을 허용한다. | 사용자 A안 선택 | `P1-GATE`, `P2-GATE`, `P3-GATE`, `prd.md` |
|
||||
| 2026-08-18 | `DEC-003` | 확정 | 신규 API, dependency, 공통 abstraction 없이 기존 fetch 경로를 연결한다. | 최소 변경 원칙과 기존 ViewModel 구조 | 전체 Goal |
|
||||
| 2026-08-25 | `DEC-004~010` | 확정 | Lottie asset·재생·크기·Reduce Motion·적용 화면을 PRD 인터뷰 결과대로 적용한다. | 사용자 인터뷰와 asset 확인 | `P4-T1`, `P4-T2`, `P4-GATE`, `prd.md` |
|
||||
| 2026-08-25 | `DEC-011` | 정정 | `DEC-003`의 공통 abstraction 제외는 Phase 1~3에 유지하고, Phase 4에서는 V2 공용 component 한 개만 허용한다. | 7개 View·20개 상태의 중복 방지 | `P4-T1`, `P4-T2`, `prd.md` |
|
||||
| 2026-08-26 | `DEC-012` | 확정 | 기존 `VStack` layout을 유지하고 refresh 상태의 `0↔64pt` 높이·offset 전환만 `0.2초 easeOut`으로 보간한다. | 기존 UI 간격 보존과 최소 변경 우선 | `P4-R3`, `P4-GATE` |
|
||||
|
||||
## 발견된 문제
|
||||
|
||||
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||
|---|---|---|---|---|---|
|
||||
| `ISSUE-001` | Low | 해결 | 현재 테스트 번들 타깃이 명확하지 않아 SwiftUI 새로고침 동작을 자동 테스트로 고정하기 어렵다. | 전체 Gate | Debug 빌드와 사용자 수동 검증을 완료 증거로 기록했다. 테스트 타깃이 추가되면 focused test를 후속 보강한다. |
|
||||
| `ISSUE-002` | High | 해결 | custom `ScrollView`의 bounce·gesture 종료·offset 측정 경로 때문에 Lottie와 refresh가 실행되지 않았다. | `P4-R1`, `P4-GATE` | bounce 강제, preference armed→release 전이, global moving-minus-fixed offset 측정으로 수정하고 사용자 동작 확인을 통과했다. |
|
||||
| `ISSUE-003` | Medium | 롤백 | frozen motion handoff는 덜컹임을 줄였지만 기존 UI 간격을 변경했다. | `P4-R2` | 사용자 결정에 따라 motion 보정과 후속 layout 수정을 모두 롤백했다. |
|
||||
|
||||
## 최종 보고 형식
|
||||
|
||||
```markdown
|
||||
구현 결과: 메인 홈, 콘텐츠, 대화 탭의 당겨서 새로고침 구현 완료 여부
|
||||
구현 결과: 메인 홈, 콘텐츠, 대화 탭의 Lottie 당겨서 새로고침 구현 완료 여부
|
||||
|
||||
- 변경: 홈/콘텐츠/대화 각 현재 선택 상태의 첫 페이지 또는 단일 조회 새로고침 연결
|
||||
- 결정: DEC-001 현재 선택 상태만 새로고침, DEC-002 빈/오류 상태 새로고침 허용, DEC-003 신규 API/dependency/공통 abstraction 제외
|
||||
- 변경: 기존 데이터 새로고침을 유지한 채 메인 7개 View의 기본 indicator를 공용 Lottie refresh component로 교체
|
||||
- 결정: DEC-006 영역 노출 중 반복, DEC-007 최대 48×48pt·64pt 영역, DEC-008~009 Reduce Motion frame 1, DEC-010 메인 7개 View 한정
|
||||
- 검증:
|
||||
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` — 성공/실패와 exit code
|
||||
- 수동 검증 — 홈/콘텐츠/대화 내부 탭·필터별 새로고침 성공/실패와 불가 사유
|
||||
- 남은 항목: 구현자가 확인한 외부 의존 또는 후속 범위, 없으면 없음
|
||||
- `xcodebuild` `SodaLive`, `SodaLive-dev` Debug simulator build — 성공/실패와 exit code
|
||||
- 수동 검증 — Lottie 재생·크기·Reduce Motion·기존 refresh 회귀·legacy 제외 결과
|
||||
- 남은 항목: 미완료 `P4-*` 또는 없음
|
||||
- 문서: `docs/20260818_메인_탭_당겨서_새로고침/prd.md`, `docs/20260818_메인_탭_당겨서_새로고침/plan-task.md`
|
||||
```
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 문서 상태 | 구현 완료 |
|
||||
| 문서 상태 | 후속 변경 구현 완료 / 사용자 수동 검증 대기 |
|
||||
| 작성일 | `2026-08-18` |
|
||||
| 최종 수정일 | `2026-08-18` |
|
||||
| 대상 제품 | 메인 홈, 콘텐츠, 대화 탭 당겨서 새로고침 |
|
||||
| 최종 수정일 | `2026-08-25` |
|
||||
| 대상 제품 | 메인 홈, 콘텐츠, 대화 탭 당겨서 새로고침 및 Lottie 표시 영역 |
|
||||
| 작성자·결정권자 | 사용자, Sisyphus |
|
||||
| 관련 API Contract | 기존 각 탭 API 계약 재사용, 신규 endpoint 없음 |
|
||||
| 관련 구현 계획 | `docs/20260818_메인_탭_당겨서_새로고침/plan-task.md` |
|
||||
@@ -234,6 +234,7 @@ MainView
|
||||
| `HOME-REFRESH-001~003` | 홈 기존 Repository | 1 | `P1-T1`, `P1-GATE` | 빌드 | 홈 내부 탭별 새로고침 |
|
||||
| `CONTENT-REFRESH-001~003` | 콘텐츠 기존 Repository | 2 | `P2-T1`, `P2-GATE` | 빌드 | 콘텐츠 내부 탭/필터별 새로고침 |
|
||||
| `CHAT-REFRESH-001~002` | `/api/v2/chat/rooms` | 3 | `P3-T1`, `P3-GATE` | 빌드 | 대화 필터별 새로고침 |
|
||||
| `REFRESH-ANIMATION-001~005` | local Lottie asset, 신규 API 없음 | 4 | `P4-T1`, `P4-T2`, `P4-GATE` | asset 검사, 두 scheme 빌드 | Lottie 재생·크기·Reduce Motion·범위 제외 |
|
||||
|
||||
## 17. Decision Log
|
||||
|
||||
@@ -251,3 +252,80 @@ MainView
|
||||
- [x] 관련 요구사항 상태·본문·수용 기준을 갱신했다.
|
||||
- [x] `plan-task.md`의 범위·Files·체크박스·완료 증거를 코드 변경 전에 갱신했다.
|
||||
- [x] 기존 Progress·review·검증 기록을 삭제하거나 덮어쓰지 않았다.
|
||||
|
||||
## 19. 후속 변경 확정: Lottie 새로고침 표시
|
||||
|
||||
### 19.1 확인된 현재 상태
|
||||
|
||||
- `2026-08-25` 사용자 요청은 기존 pull-to-refresh 동작의 표시 영역을 `Resources/pull-to-refresh.json` 기반 Lottie animation으로 변경하는 것이다.
|
||||
- 구현 전 pull-to-refresh는 메인 홈·콘텐츠·대화의 7개 SwiftUI View에 있고, loading·empty·error·content 분기를 합쳐 `.refreshable` modifier가 20곳에 적용되어 있었다.
|
||||
- Lottie `4.6.1` Swift Package가 `SodaLive`, `SodaLive-dev` 두 target에 연결되어 있다.
|
||||
- 공식 Lottie iOS API는 SwiftUI용 `LottieView`와 local JSON loading, playback mode 및 progress 제어를 제공한다.
|
||||
- `SodaLive/Resources/pull-to-refresh.json`은 `150×150`, `30fps`, frame `0..<45`, marker 없음인 local Lottie asset이며 `SodaLive`, `SodaLive-dev` 두 target의 Resources build phase에 포함되어 있다.
|
||||
- asset의 frame `0`은 trim path 시작·끝 값이 모두 `3`이라 비어 있고, frame `1`부터 stroke가 표시된다.
|
||||
- Phase 4 요구사항 인터뷰 요청에서는 문서 작성만 수행했고, 이후 사용자 승인으로 코드 구현과 자동 검증을 완료했다.
|
||||
|
||||
### 19.2 후속 요구사항
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `REFRESH-ANIMATION-001` | 확정 | 기존 메인 홈·콘텐츠·대화의 pull-to-refresh 표시 영역에 지정된 Lottie animation을 사용한다. | 기존 7개 View의 모든 refresh 가능 상태에서 동일한 Lottie asset이 표시되고 기존 데이터 새로고침 동작은 유지된다. | `P4-T1`, `P4-T2` |
|
||||
| `REFRESH-ANIMATION-002` | 확정 | 당김 영역이 보이면 Lottie animation을 반복 재생하고, 새로고침 완료 또는 당김 취소로 영역이 사라질 때 정지·처음 frame으로 초기화한다. | 당기는 거리에 따른 progress 제어 없이 영역 노출 중 반복 재생하며, 영역이 사라졌다가 다시 나타나면 처음부터 재생한다. | `DEC-006`, `P4-T1` |
|
||||
| `REFRESH-ANIMATION-003` | 확정 | Lottie animation을 원본 비율을 유지한 최대 `48×48pt`로 중앙 정렬하고 상하 `8pt` 여백을 둔다. | 총 `64pt` 높이의 refresh 영역에서 animation이 늘어나거나 잘리지 않고 iPhone·iPad에 동일하게 표시된다. | `DEC-007`, `P4-T1`, `P4-GATE` |
|
||||
| `REFRESH-ANIMATION-004` | 확정 | Reduce Motion 활성화 시 반복 재생하지 않고 asset의 첫 번째 유효 frame인 frame `1`을 표시한다. | 동작 줄이기가 켜지면 animation이 재생되지 않으며, frame `1`의 stroke가 영역 노출 중 정지 상태로 표시된다. | `DEC-008`, `DEC-009`, `P4-T1`, `P4-GATE` |
|
||||
| `REFRESH-ANIMATION-005` | 확정 | Lottie 표시를 기존 PRD 범위인 메인 홈·콘텐츠·대화 7개 View에만 적용한다. | 7개 View의 20개 refresh 가능 상태에서 Lottie만 표시하고 기존 indicator를 함께 노출하지 않으며, `LiveNowAllView`와 `ContentDetailView`는 변경되지 않는다. | `DEC-010`, `P4-T2`, `P4-GATE` |
|
||||
|
||||
### 19.3 후속 Open Questions
|
||||
|
||||
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 기한/시점 | 영향 Goal |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `OQ-002` | 확정 | 당기는 중과 refresh 실행 중 Lottie playback을 각각 어떻게 제어할지 | 영역 노출 중 반복 재생, 영역 종료 시 정지·초기화 | 사용자 | 2026-08-25 | 후속 Phase 전체 |
|
||||
| `OQ-003` | 확정 | Lottie 표시 크기, refresh 영역 높이와 수평·수직 정렬 | 최대 `48×48pt`, 원본 비율 유지, 중앙 정렬, 상하 `8pt` 여백 | 사용자 | 2026-08-25 | 후속 UI Gate |
|
||||
| `OQ-004` | 확정 | iOS Reduce Motion 활성화 시 표시할 정지 frame | 비어 있는 frame `0` 대신 첫 번째 유효 frame `1` 표시 | 사용자 | 2026-08-25 | 후속 접근성 Gate |
|
||||
| `OQ-005` | 확정 | 메인 7개 View만 적용할지, 다른 기존 pull-to-refresh 화면도 포함할지 | 기존 PRD 범위인 메인 7개 View만 적용 | 사용자 | 2026-08-25 | 후속 Phase 전체 |
|
||||
|
||||
### 19.4 후속 변경 제약
|
||||
|
||||
- 기존 `refresh()` 호출, 선택 탭·필터 유지, pagination 초기화와 오류 처리 동작은 변경하지 않는다.
|
||||
- Lottie `4.6.1` 외에 새 dependency를 추가하지 않는다.
|
||||
- 20개 modifier마다 구현을 복제하지 않고, 기존 7개 View의 공통 요구를 충족하는 최소 재사용 단위만 구현한다.
|
||||
- SwiftUI `.refreshable`의 기본 indicator는 custom Lottie로 교체할 공개 API가 없으므로, V2 공용 refresh component 한 개에서 당김 offset·비동기 refresh 상태·Lottie 표시를 관리한다.
|
||||
- 기존 `RefreshableScrollView` `1.1.1`은 indicator가 내부에 고정되어 있으므로 후속 V2 component의 기반으로 사용하지 않고, package source도 수정하지 않는다.
|
||||
- Lottie visual은 접근성 트리에서 숨기고, 공용 refresh component는 기존 `I18n.LiveNow.refreshButton` 문구를 재사용한 명시적 accessibility refresh action을 제공한다.
|
||||
- `SodaLive.xcodeproj/project.pbxproj`, `SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved`의 현재 Lottie 관련 변경은 사용자 작업으로 보존한다.
|
||||
- `SodaLive/Sources/Live/Now/All/LiveNowAllView.swift`, `SodaLive/Sources/Content/Detail/ContentDetailView.swift`의 기존 pull-to-refresh는 이번 범위에서 제외한다.
|
||||
|
||||
### 19.5 후속 변경 Decision Log
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-25 | `DEC-004` | 확정 | 기존 iOS 기본 refresh 표시를 지정된 Lottie animation으로 변경하고, 이미 추가된 Lottie `4.6.1`을 사용한다. | 사용자 직접 요청과 현재 Swift Package 설정 | `REFRESH-ANIMATION-001~004` |
|
||||
| 2026-08-25 | `DEC-005` | 확정 | Phase 4 요구사항 인터뷰 실행에서는 문서 작성만 수행하고 코드 구현은 하지 않는다. | 사용자 직접 요청 | 후속 Phase 전체 |
|
||||
| 2026-08-25 | `DEC-006` | 확정 | Lottie는 당김 영역이 보이는 동안 반복 재생하고, 영역이 사라질 때 정지 후 처음 frame으로 초기화한다. | 사용자 인터뷰 A안 선택 | `REFRESH-ANIMATION-002`, `OQ-002` |
|
||||
| 2026-08-25 | `DEC-007` | 확정 | Lottie는 원본 비율을 유지한 최대 `48×48pt`로 중앙 정렬하고, 상하 `8pt` 여백을 포함한 총 `64pt` refresh 영역에 표시한다. | 사용자 인터뷰 A안 선택과 기존 `SodaSpacing` 값 | `REFRESH-ANIMATION-003`, `OQ-003` |
|
||||
| 2026-08-25 | `DEC-008` | 확정 | iOS Reduce Motion이 활성화되면 Lottie를 반복 재생하지 않고 정지 frame을 표시한다. | 사용자 인터뷰 A안 선택 | `REFRESH-ANIMATION-004`, `OQ-004` |
|
||||
| 2026-08-25 | `DEC-009` | 확정 | Reduce Motion 정지 화면은 비어 있는 frame `0`을 건너뛰고 asset의 첫 번째 유효 frame인 frame `1`을 사용한다. | 사용자 인터뷰 A안 선택과 asset frame 확인 | `REFRESH-ANIMATION-004`, `OQ-004` |
|
||||
| 2026-08-25 | `DEC-010` | 확정 | Lottie 적용 범위는 기존 PRD의 메인 홈·콘텐츠·대화 7개 View로 제한하고 legacy pull-to-refresh 2개 화면은 제외한다. | 사용자 인터뷰 A안 선택 | `REFRESH-ANIMATION-005`, `OQ-005` |
|
||||
| 2026-08-25 | `DEC-011` | 정정 | `DEC-003`의 공통 refresh abstraction 제외는 Phase 1~3에 유지하되, Phase 4에서는 7개 View·20개 상태의 표시 중복을 막기 위한 V2 공용 component 한 개만 허용한다. | 동일한 Lottie 상태·접근성·비동기 guard를 한 곳에서 유지하기 위한 최소 재사용 단위 | `P4-T1`, `P4-T2` |
|
||||
|
||||
### 19.6 인터뷰 종료 결과
|
||||
|
||||
| 차원 | 명확성 |
|
||||
|---|---:|
|
||||
| Goal | `1.00` |
|
||||
| Scope | `1.00` |
|
||||
| Constraints | `0.90` |
|
||||
| Success | `1.00` |
|
||||
| Context | `0.90` |
|
||||
|
||||
- 최종 모호성: `0.03`
|
||||
- 결정사항: `DEC-004~011`에 Lottie asset·재생·크기·Reduce Motion·적용 범위·최소 공용 component를 기록했다.
|
||||
- 열린 질문: 없음.
|
||||
|
||||
### 19.7 후속 성공 기준
|
||||
|
||||
- [x] V2 공용 Lottie refresh component 2개가 `SodaLive`, `SodaLive-dev` 두 target에서 빌드된다. (`REFRESH-ANIMATION-001~004`)
|
||||
- [x] 메인 7개 View의 기존 `.refreshable` 20곳이 공용 component로 교체되고 기본 indicator 경로가 남아 있지 않다. (`REFRESH-ANIMATION-001`, `REFRESH-ANIMATION-005`)
|
||||
- [x] 기존 7개 ViewModel과 `LiveNowAllView`, `ContentDetailView`에 구현 diff가 없다. (`REFRESH-ANIMATION-005`)
|
||||
- [ ] 실제 당김·취소·새로고침 완료에서 Lottie 반복·초기화와 단일 요청 동작을 확인한다. (`REFRESH-ANIMATION-002`)
|
||||
- [ ] Reduce Motion frame `1`, 최대 `48×48pt`, 총 `64pt` 영역과 iPhone·iPad 레이아웃을 확인한다. (`REFRESH-ANIMATION-003~004`)
|
||||
|
||||
Reference in New Issue
Block a user