├── Sample ├── CustomButton │ ├── CustomButton │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── ViewController.swift │ │ ├── StoryboardBasedViewController.swift │ │ ├── Info.plist │ │ ├── AppDelegate.swift │ │ ├── MarrytingButton.swift │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── SceneDelegate.swift │ │ └── CodeBasedViewController.swift │ └── CustomButton.xcodeproj │ │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── ReactorKitSampleProject │ └── ReactorKitSample │ ├── ReactorKitSample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── Info.plist │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── CounterViewReactor.swift │ ├── ViewController.swift │ └── PulseViewController.swift │ ├── ReactorKitSample.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── ReactorKitSampleTests │ └── ReactorKitSampleTests.swift ├── TIL ├── 2022 │ ├── WWDC21_discover_concurrency_in_swiftui.md │ ├── TIL_2022:09:30_state_vs_behavior_test.md │ ├── TIL_2022:10:24_viewcontroller_lifecycle.md │ ├── TIL_2022:10:21_making_core_animation.md │ ├── TIL_2022:10:16_protocol_inherit_class.md │ ├── TIL_2022:10:14_tuist_layout_flexlayout_build_error.md │ ├── TIL_2022:10:23_making_core_animation.md │ ├── TIL_2022:10:10_upload_image_with_typora.md │ ├── TIL_2022:10:15_test_uinavigationcontroller.md │ ├── TIL_2022:10:20_core_animation.md │ ├── TIL_2022:10:04_hig_feedback.md │ ├── TIL_2022:10:07_Coding_Test.md │ ├── TIL_2022:10:19_explore_ui_animation_hitches_and_the_render_loop.md │ ├── TIL_2022:10:22_making_core_animation.md │ ├── TIL_2022:10:12_dynamic_tableviewcell_of_tableview_in_scrollview_height_not_worked.md │ ├── TIL_2022:10:16_button_configuration_more.md │ ├── WWDC22_introduce_charts.md │ ├── WWDC21_discover_and_curate_swift_packages_using_collections.md │ ├── TIL_2022:10:19_advanced_graphics_and_animations_for_ios.md │ ├── TIL_2022:10:05_xcactivity.md │ ├── TIL_2022:10:19_blur_effect_3_ways.md │ ├── TIL_2022:10:02_reactorkit_pulse.md │ ├── TIL_2022:10:11_task_init_cancel.md │ ├── TIL_2022:10:19_uibezierpath.md │ ├── TIL_2022:10:01_test_doubles_dummy_fake_stub_mock_spy.md │ ├── TIL_2022:10:10_heap.md │ ├── WWDC21_protect_mutable_state_with_swift_actors.md │ ├── TIL_2022:10:11_concurrency_swift5.5_basic.md │ ├── TIL_2022:10:02_swift_naming.md │ ├── WWDC22_swift_charts_raise_the_bar.md │ └── TIL_2022:10:13_pinlayout_flexlayout.md └── 2025 │ ├── TIL_2025_11_14_StoreKit2.md │ ├── TIL_2025_11_19_dynamicMemberLookup.md │ ├── TIL_2025_11_20_LinkBinaryWithLibraries.md │ └── TIL_2025_11_17_instrument_swiftui.md ├── .gitignore └── README.md /Sample/CustomButton/CustomButton/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TIL/2025/TIL_2025_11_14_StoreKit2.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | - 블로그 작성 6 | - [StoreKit2 기반 인앱결제/구독 정리하기](https://wodyios.tistory.com/93) 7 | -------------------------------------------------------------------------------- /TIL/2025/TIL_2025_11_19_dynamicMemberLookup.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | - 블로그 작성 6 | - [@dynamicMemberLookup](https://wodyios.tistory.com/95) 7 | -------------------------------------------------------------------------------- /TIL/2025/TIL_2025_11_20_LinkBinaryWithLibraries.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | - 블로그 작성 6 | - [Link Binary with Libraraies](https://wodyios.tistory.com/97) 7 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TIL/2022/WWDC21_discover_concurrency_in_swiftui.md: -------------------------------------------------------------------------------- 1 | ### Discover concurrency in SwiftUI 2 | 3 | ---- 4 | 5 | Swift5.5 concurrency 6 | 7 | 8 | 9 | Concurrent data models (동시성 데이터 모델) -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CustomButton 4 | // 5 | // Created by Woody on 2022/07/06. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:09:30_state_vs_behavior_test.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.09.30 (금) 6 | 7 |
8 | 9 |
10 | 11 | Test Doubles에 대해 알아보기 전에, 상태 기반 vs 행위 기반 테스트에 대해 공부해보았다. 12 | 13 | 👉🏻 🔗 https://www.wodyd.com/unit-testing-behavior-vs-state/ 14 | 15 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/StoryboardBasedViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryboardBasedViewController.swift 3 | // CustomButton 4 | // 5 | // Created by Woody on 2022/07/07. 6 | // 7 | 8 | import UIKit 9 | 10 | class StoryboardBasedViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:24_viewcontroller_lifecycle.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.24 (월) 6 | 7 | 8 | 9 | 뷰컨트롤러의 생명주기를 다시 살펴보고 공부내용은 [블로그](https://wodyios.tistory.com/74)에 작성했습니다. ViewWillAppear 뒤에 ViewDidAppear 메소드가 항상 호출되지 않는다, 그리고 ViewWillDisappear 메소드 뒤에 ViewDidDisappear 메소드가 항상 호출되지 않는다는 사실을 알았습니다. Will 뒤에는 항상 Did가 오진 않지만 Did 앞에는 항상 Will이 와야한다는 사실 기억합시다!! 오늘은 프로젝트에 시간을 많이 보낸 것 같네요. 이번주 금요일이 중간 발표(?)다보니 팀원 모두 열심히 개발하고 있는 것 같아요. 저도 오늘은 집중해서 맡은 구현은 완료해야겠습니당 -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:21_making_core_animation.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.21 (금) 6 | 7 |
8 | 9 | 오늘은 아래 유튜브에 나오는 것을 만들어보았습니다. 요새 어떻게 알았는지 유튜브 알고리즘에 크리에이티브 디자인 띵킹의 주제가 많이 추천되고 있습니다. 그 중 하나인데 "절대 만나지 않는 로딩"을 css로만 만든 영상입니다. 저는 이것을 swift로 만들어보고 약간의 변형을 주었습니다.. 구현을 하다 anchorPoint와 position의 관계가 궁금해져 공부를 하고 [블로그](https://wodyios.tistory.com/71)에 작성했습니다. 10 | 11 | 유튜브 링크 -> https://www.youtube.com/watch?v=1Aq9OJuS3ok&t=245s 12 | 13 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // ReactorKitSample 4 | // 5 | // Created by Woody on 2022/08/04. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let scene = (scene as? UIWindowScene) else { return } 16 | let window = UIWindow(windowScene: scene) 17 | let root = PulseViewController() 18 | root.reactor = PulseReactor() 19 | window.rootViewController = root 20 | 21 | self.window = window 22 | window.makeKeyAndVisible() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSampleTests/ReactorKitSampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactorKitSampleTests.swift 3 | // ReactorKitSampleTests 4 | // 5 | // Created by Woody on 2022/08/05. 6 | // 7 | 8 | import XCTest 9 | import ReactorKitSample 10 | 11 | class ReactorKitSampleTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | 23 | } 24 | 25 | func testPerformanceExample() throws { 26 | // This is an example of a performance test case. 27 | measure { 28 | // Put the code you want to measure the time of here. 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:16_protocol_inherit_class.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.15 (토) 6 | 7 | 8 | 9 | 프로토콜이 UIView를 상속받을 수 있을까? 디자인컴포넌트를 만들 때, 팀원들이 컴포넌트를 바꾸지 말고 원하는 기능만 수행하게 만들고 싶은 상황입니다. 그래서 프로토콜 안에 해당 기능들을 담보하게 하는 방법이 가장 일반적으로 생각할 수 있습니다. 10 | 11 | 12 | 13 | Testable한 코드 작성하는 방법 14 | 15 | - Protocols and parameterization 16 | - Separating logic and effects 17 | 18 | 19 | 20 | #### Protocols and parameterization 21 | 22 | 1. 뷰컨트롤러는 리소스가 크기 때문에 로직테스트에서는 생성할 필요가없다. 비즈니스 로직을 뷰컨트롤러에서 분리시키자 23 | 2. 뷰의 프로퍼티에 대한 값을 간접적으로 입력 받자 24 | 3. UIApplication 인스턴스와 같이 테스트가 불가능한 부분은 Mock데이터를 만들어서 테스트하자 25 | 4. 테스트하는 함수의 Input을 제어할 수 있도록 하자. 26 | 5. 27 | 28 | - 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | - https://stackoverflow.com/questions/58852513/protocol-inheritance-from-class 37 | - https://developer.apple.com/documentation/xcode-release-notes/swift-5-release-notes-for-xcode-10_2 38 | - https://www.swiftbysundell.com/articles/specializing-protocols-in-swift/ 39 | - https://twitter.com/johnsundell/status/1110491386636308480?lang=en -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "reactorkit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/ReactorKit/ReactorKit.git", 7 | "state" : { 8 | "revision" : "8fa33f09c6f6621a2aa536d739956d53b84dd139", 9 | "version" : "3.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "rxswift", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/ReactiveX/RxSwift.git", 16 | "state" : { 17 | "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", 18 | "version" : "6.5.0" 19 | } 20 | }, 21 | { 22 | "identity" : "weakmaptable", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/ReactorKit/WeakMapTable.git", 25 | "state" : { 26 | "revision" : "cb05d64cef2bbf51e85c53adee937df46540a74e", 27 | "version" : "1.2.1" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /TIL/2025/TIL_2025_11_17_instrument_swiftui.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | #### SwiftUI Instrument 소개 6 | 7 | - SwiftUI는 선언형 프레임워크로 UIKit처럼 Back trace로 디버깅하기 쉽지 않음 8 | - 기본적으로 뷰가 언제업데이트되는지 찾기 어려움 9 | - Xcode 26부터 SwiftUI를 인스트루먼트할 수 있는 기능 제공 10 | - 프로파일링에서 볼 수 있는 정보들 11 | - Long View Body Updates: body가 실행되는데 오래 걸릴 때 표시 12 | - Long Representable Updates: View, ViewController가 실행되는데 오래 걸릴 때 표시 13 | - Other Updates: 그 외 업데이트가 오래 걸릴 때 표시 14 | - 참고) 만약 Time Profiler에 그래프가 있는데, SwiftUI Profiler엔 아무런 문제가 없다면 뷰 외 다른 문제가 있다는 뜻 15 | 16 | #### Time Profile 디버깅 방식 17 | 18 | - SwiftUI 뷰가 계속 업데이트 되는 부분 영역 선택 19 | - 선택된 영역의 Timer Profiler 클릭 20 | - Time Profiler에서 계속 업데이트가 되는 View 이름 검색 후 Stack Trace 확인 21 | - Stack Trace에서 왼쪽 Weight의 차이가 큰 부분 찾기 22 | - 차이가 큰 부분의 코드 확인하기 23 | 24 | #### Cause and Effects 기능 25 | 26 | - SwiftUI가 뷰를 업데이트하는 방식 27 | - 뷰 구조체에 대한 뷰 트리를 먼저 생성 28 | - @State, @ObservedObject 등을 가진 속성을 저장소에 저장 29 | - 속성이 변하면 새로운 뷰트리 생성후 기존 뷰트리와 비교하며 diffing 알고리즘 실행 30 | - 기존 속성에 대한 Transaction 생성하고, 영향 받는 뷰들에 outdated 표시 31 | - 새로 변경된 속성에 따라 outdated 표시된 뷰를 전부 비교 32 | - 변경된 UI 업데이트 33 | - 뷰가 언제 업데이트하는지 breakpoint를 걸어서 찾을 수 없음. 그래서 Cause and Effects 기능을 제공함 34 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:14_tuist_layout_flexlayout_build_error.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.14 (금) 6 | 7 | 8 | 9 | ### Tuist 빌드 시 오류 발생한 라이브러리 10 | 11 | - [FlexLayout](https://github.com/layoutBox/FlexLayout) 12 | - SPM으로 설정시 "Not Found "yoga.m" 오류 발생 13 | - 원인을 찾을 수 없었기 때문에 Carthage로 변경 -> 해결 14 | 15 | - [PlinLayout](https://github.com/layoutBox/PinLayout) 16 | 17 | - PinLayout 라이브러리 경로를 찾지 못함 18 | 19 | ```bash 20 | # 캐시처리가 되었지만 라이브리러를 다운받지 않은 상태에서 캐시처리가 되었기 때문에 에러 발생하여 Dependencies 삭제하고 빌드함 21 | $ rm -rf Tuist/Dependencies 22 | $ tuist fetch 23 | ``` 24 | 25 | - Carthage로 빌드시 projphx 파일을 찾을 수 없다는 오류를 만남 26 | 27 | ```bash 28 | # PinLayout에서 해당 폴더를 빌드하기 때문에 에러 발생하여, TestProjects를 삭제하고 Carthage를 이용하여 PinLayout를 빌드함. 29 | $ rm -rf Tuist/Dependencies/Carthage/Checkouts/PinLayout/TestProjects 30 | $ carthage build PinLayout --project-directory Tuist/Dependencies --platform iOS --use-xcframeworks --no-use-binaries --use-netrc --cache-builds --verbose 31 | 32 | # 계속 서드파티 라이브러리를 빌드함 33 | $ tuist fetch 34 | ``` 35 | 36 | 37 | 38 | ### 기본적인 오류 해결 39 | 40 | ```bash 41 | $ tuist clean 42 | $ tuist fetch 43 | $ tuist generate 44 | ``` 45 | 46 | 47 | 48 | ### 참고 49 | 50 | - https://github.com/minsOne/iOSApplicationTemplate 51 | - https://github.com/layoutBox/FlexLayout 52 | - https://github.com/layoutBox/PinLayout -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:23_making_core_animation.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.23 (일) 6 | 7 | 어제 애니메이션 구현을 완료하고 오늘은 반짝이는 별 애니메이션을 구현해보았습니다. 구현 방법에 대해서 [블로그](https://wodyios.tistory.com/73)에 작성해보았습니다. 하지만 구현하면서 생각한 것은 애니메이션을 만드는 것은 단순히 개발의 영역이 아니라는 것입니다. 속도와 지연 시간, 위치 등등 여러가지 요소들을 종합하여 만들어지는 하나의 애니메이션인데 혼자 이것저것 프로퍼티 값을 바꿔가면서 애니메이션을 만들고 있으니 이렇게 하는 게 맞는 건가 싶었습니다.. 그런데 하나하나 조절해가면서도 재밌고 설레면 이 일이 잘 맞는 거겠죠? 이제 인터렉션이 섞인 애니메이션을 구현해보는 일만 남았는데 이 단기간 프로젝트가 끝나더라도 한달에 하나씩은 저만의 애니메이션을 구현해봐야곘습니다! 8 | 9 | 10 | 11 | 12 | 13 | ---- 14 | 15 | ### 랜덤 함수 16 | 17 | swift에서 제공하는 랜덤함수 종류는 **`arc4random()` / `arc4random_uniform(UInt32)` / `drand48()`**가 있습니다. 세가지 모두 성격이 다릅니다. 첫 번째는 **0 ~ 2^32-1** 사이의 랜덤 UInt32 타입의 값을 리턴합니다. 두 번째는 0 ~ 입력한 값-1 사이의 랜덤 UInt32 타입의 값을 리턴합니다. 마지막으로는 **0~1.0** 사이의 랜덤 Double 타입을 리턴합니다. 18 | 19 | 그럼 이를 이용해서 프레임에서 랜덤한 위치를 표현하기 위해서는 아래의 코드를 작성할 수 있습니다. `arc4random()`를 이용해서 난수 하나를 뽑고 width와 height로 나눈 나머지로 구할 수 있습니다. 20 | 21 | ```swift 22 | func generateRandomPosition() -> CGPoint { 23 | let height = frame.height 24 | let width = frame.width 25 | 26 | return CGPoint( 27 | x: CGFloat(arc4random()).truncatingRemainder(dividingBy: width), 28 | y: CGFloat(arc4random()).truncatingRemainder(dividingBy: height) 29 | ) 30 | } 31 | ``` 32 | 33 | ### Ref 34 | 35 | - https://zeddios.tistory.com/214 36 | 37 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CustomButton 4 | // 5 | // Created by Woody on 2022/07/06. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ReactorKitSample 4 | // 5 | // Created by Woody on 2022/08/04. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/MarrytingButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarrytingButton.swift 3 | // CustomButton 4 | // 5 | // Created by Woody on 2022/07/06. 6 | // 7 | 8 | import UIKit 9 | 10 | /// 코드 11 | final class MarrytingButton: UIButton { 12 | private let selectedTextColor: UIColor = UIColor.white 13 | private let unselectedTextColor: UIColor = .init(rgb: 0x242424) 14 | private let disabledTextColor: UIColor = .init(rgb: 0xBABABA) 15 | 16 | private let disabledBackgroundColor: UIColor = .init(rgb: 0x949494) 17 | private let defaultBackgroundColor: UIColor = .init(rgb: 0xC6DC84) 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | 22 | } 23 | 24 | func layout() { 25 | 26 | } 27 | 28 | func attribute() { 29 | 30 | } 31 | 32 | required init?(coder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | } 36 | 37 | extension UIColor { 38 | convenience init(red: Int, green: Int, blue: Int) { 39 | assert(red >= 0 && red <= 255, "Invalid red component") 40 | assert(green >= 0 && green <= 255, "Invalid green component") 41 | assert(blue >= 0 && blue <= 255, "Invalid blue component") 42 | 43 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) 44 | } 45 | 46 | convenience init(rgb: Int) { 47 | self.init( 48 | red: (rgb >> 16) & 0xFF, 49 | green: (rgb >> 8) & 0xFF, 50 | blue: rgb & 0xFF 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:10_upload_image_with_typora.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.10 (월) 6 | 7 | 8 | 9 | Typora로 이미지 바로 업로드하기 10 | 11 | 타이포라로 글을 작성하는데 매번 이미지를 올릴 떄마다 너무 귀찮았습니다. 처음엔 글을 다 작성한 후 이미지를 하나하나 다 올려줬는데 올린 후 이미지 위치와 사이즈 조절까지... 👿 그래서 바로 이미지 올리는 방법을 오늘 적용해봤습니당 12 | 13 | 먼저, upic이라는 중국에서 만든 프로그램을 다운받아야합니다. 터미널에 명령어 `brew install bigwig-club/brew/upic --cask` 입력해주세요. 그리고 프로그램을 실행하게 되면, 아래와 같이 메뉴바에서 upic 프로그램을 볼 수 있고 선택후 `Preferences` -> `Host`를 눌러줍니다. 14 | 15 |
16 | 17 | 아래와 같은 설정 창이 나옵니다. 처음에는 SMMS라는 것만 호스트에 존재합니다. SMMS를 지웁니다. 지워주지 않으면 나중에 메뉴바에서 upic 선택 후 Github을 선택해야 업로드가 정상적으로 동작이 됩니다. (더 귀찮음..) Owner는 내 github 닉네임, Repo는 내 github 저장소, Token은 Github에서 발급받은 토큰입니다. 토큰을 발급받는 방식은 생략할게요. 그리고 Save를 눌러줍니다. 18 | 19 |
20 | 21 | 22 | 23 | 마지막으로, Typora의 설정에서 이미지 -> 이미지 업로드 설정의 이미지 업로더를 uPic으로 선택하고 재실행합니다. 그럼 이제 타이포라에서 이미지를 복사 붙여넣기 하면 이미지 업로드라는 항목이 나오고 선택하면 바로 지정 레포지토리에 업로드 후, URL을 가져옵니다. 🎉 24 | 25 |

26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | - https://kilbong0508.tistory.com/450 -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:15_test_uinavigationcontroller.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.15 (토) 6 | 7 | 8 | 9 | **UINavigationController 테스트하기** 10 | 11 | iOS에서 화면전환은 UINavigationController을 이용하거나 present 메소드를 이용합니다. 화면전환이 제대로 이루어졌다는 것을 테스트하려면 먼저 MockNavigationController를 만듭니다. 12 | 13 | ```swift 14 | final class MockNavigationController: UINavigationController { 15 | var pushViewControllerCalled: Bool = false 16 | var pushedViewController: UIViewController? 17 | override func pushViewController(_ viewController: UIViewController, animated: Bool) { 18 | super.pushViewController(viewController, animated: animated) 19 | pushViewControllerCalled = true 20 | pushedViewController = viewController 21 | } 22 | } 23 | ``` 24 | 25 | 만든 Mock 데이터를 코디네이터에 주입하여 해당 메소드가 잘 실행되었는 지 확인합니다. 26 | 27 | > 아래 코드에선 푸쉬 메소드가 제대로 불렸는 지, 그리고 푸쉬된 VC 타입을 확인해주는데 푸쉬된 VC의 타입만 확인해주면 제대로 푸쉬되었는 지 알 수 있습니다. 28 | 29 | ```swift 30 | final class IntroCoordinatorTests: XCTestCase { 31 | var sut: IntroCoordinatorProtocol! 32 | var mockNavigationController: MockNavigationController! 33 | 34 | override func setUp() { 35 | super.setUp() 36 | mockNavigationController = MockNavigationController(rootViewController: .init()) 37 | sut = IntroCoordinator(navigationController: mockNavigationController) 38 | } 39 | 40 | override func tearDown() { 41 | sut = nil 42 | super.tearDown() 43 | } 44 | 45 | func testIntroCoordinator_이메일입력_화면으로_전환합니다() { 46 | // when 47 | sut.coordinateToEnterEmailScene() 48 | 49 | // then 50 | XCTAssertEqual(mockNavigationController.pushViewControllerCalled, true) 51 | XCTAssertTrue(mockNavigationController.pushedViewController is EnterEmailViewController) 52 | } 53 | } 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:20_core_animation.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.20 (목) 6 | 7 | 오늘부터 애니메이션 프로젝트를 시작합니다:) 🚀 이번 프로젝트에서 애니메이션을 조금 많이 쓰다보니 어쩌다 시작하게 됐지만 애니메이션을 뿌셔보겠습니다. 오늘은 방금 간단하게 Core Animation을 이용해서 로딩 애니메이션을 만들었습니다. Core Animation에는 정말 여러 종류의 애니메이션이 존재하는데 그 중에서 오늘 사용해본 것들은 아래와 같습니다. 또한 구현을 하다보니 CAReplicatorLayer에 대해서도 공부하게 되었는데 자세한 내용은 [블로그](https://wodyios.tistory.com/69)에 작성했습니다. 8 | 9 | 계속 문서를 읽으면서 애니메이션을 하나씩 구현해보고 프로젝트로 정리하여 올리려고 합니다. 어디까지 만들어볼 수 있을까요! 10 | 11 | 12 | 13 | ---- 14 | 15 | ### CABasicAnimation 16 | 17 | CAKeyframeAnimation과 마찬가지로 keypath를 이용해서 초기화합니다. 해당 내용은 [블로그](https://wodyios.tistory.com/25)에 작성했습니다. 18 | 19 | ### CAKeyframeAnimation 20 | 21 | keypath를 이용해서 초기화합니다. `values`와 `keyTimes`프로퍼티를 사용해서 애니메이션을 지정합니다. 그럼 Core Animation은 지정한 값과 현재값 사이의 값들을 생성해서 애니메이션을 실행합니다. 22 | 23 | ```swift 24 | let colorKeyframeAnimation = CAKeyframeAnimation(keyPath: "backgroundColor") 25 | colorKeyframeAnimation.values = [UIColor.red.cgColor, 26 | UIColor.green.cgColor, 27 | UIColor.blue.cgColor] 28 | colorKeyframeAnimation.keyTimes = [0, 0.5, 1] 29 | colorKeyframeAnimation.duration = 2 30 | ``` 31 | 32 | ### CASprintAnimation 33 | 34 | 이것도 마찬가지로 keypath를 이용해서 초기화합니다. 이 객체이는 스프링을 조절할 수 있는 프로퍼티가 있습니다. `damping`, `initialVelocity`, `mass`, `settlingDuration`, `stiffness` 입니다. 프로퍼티에 관한 내용은 문서를 참고해주세요! 35 | 36 | ```swift 37 | let animation = CASpringAnimation(keyPath: "path") 38 | animation.fromValue = createPath(point: fromPoint) 39 | animation.toValue = createPath(point: .init(x: view.bounds.maxX / 2, y: 100)) 40 | animation.initialVelocity = 20 41 | animation.stiffness = 1000 42 | animation.duration = animation.settlingDuration 43 | ``` 44 | 45 | 46 | 47 | 48 | 49 | - 애플 문서 참고 50 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:04_hig_feedback.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022년 10월 04일 (화) 6 | 7 |
8 | 9 | [피드백에 대한 HIG](https://developer.apple.com/design/human-interface-guidelines/patterns/feedback/)를 읽으며 작성한 내용입니다. 직독직해를 한 부분도 있고 의역을 한 부분도 있기에 정확한 해석이 아닐 수도 있습니다. 틀린 부분이 있다면 메일로 피드백 부탁드립니다. 10 | 11 | Feedback(피드백)은 무엇이 일어났는지 다음에 어떤 행동을 할 수 있을지, 실수를 어떻게 피하는지, 행위에 대한 결과 등을 알 수 있게 해줍니다. 명확하고 일정한 **피드백**은 사용자들로 하여금 앱에 대해 더 직관적이고 더 깊은 이해를 할 수 있도록 합니다. 피드백은 여러가지 다른 방법으로 사용자들과 소통합니다. 12 | 13 | - 현재 상태 14 | - 중요한 task나 action에 대한 성공, 실패 15 | - 실패 결과를 일으킬 수 있는 action에 대한 경고문 16 | - 오해와 문제 상황을 고칠 수 있는 기회 17 | 18 | 가장 효과적인 피드백은 명확한 정보를 전달하는 것입니다. 예를 들어, 사용자가 필요한 상황에 올바른 방법으로 상태 정보를 전달하는 상황이 있습니다. 반대로, 데이터를 잃을 수 있다는 경고문을 보여주면서, 사용자에게 문제를 피할 수 있는 기회를 방해하는 것은 좋지 않은 상황입니다. 19 | 20 | ### Best practices 21 | 22 | **모든 피드백은 모든 상황에서 가능해야 합니다.** 만약 너가 여러 방법으로 피드백을 제공할 때, 많은 사용자들에게 그들의 상황에 맞게 제공되어야 합니다. 예를 들어, 색깔, 텍스트, 소리, 햅틱 등을 사용한 피드백을 제공할 때, 사용자는 디바이스를 무음처리할 때, 스크린을 보고 있지 않을 때 또는 보이스오버를 사용하고 있을 때 등 어떤 상황에도 관여하지 않고 해당 피드백을 받아야 합니다. 23 | 24 | **상태 피드백(status feedback)은 인터페이스에 녹여내는게 좋습니다.** 상태 피드백이 묘사하는 아이템 옆에 위치한다면, 사용자들은 어떠한 액션을 취하지 않고서도 중요한 정보를 얻을 수 있습니다. 예를 들어, iOS 기본 앱 Mail은 업데이트된 그리고, 읽지 않은 메시지 개수를 mailbox 아이콘 옆에 위치시켜, 사용자들이 필요한 정보를 눈에 거슬리지 않으면서 보여주는 경우가 있습니다. 25 | 26 | **아주 치명적인 정보엔 alert를 사용해야 합니다.** alert는 현재 contextf를 방해하므로, 방해할 만한 정도의 정보인 지 중요도를 체크해봐야 합니다. Alert는 너무 자주 사용하면 impact가 없어지고, 중요하지 않은 정보까지 전달하게 됩니다. 이 Best Pactice는 아주 "치명적인" 정보에"만" alert를 사용해야 한다는 소리입니다. 27 | 28 | **사용자들이 의도치않게 데이터 손실을 유발할 수 있는 행동을 한다면 경고합니다.** 반대로 의도한 데이터 손실의 경우에는 경고하지 말아야 합니다. 예를 들어, 파인더에서 파일을 휴지통에 지우는 행위는 예상되는 결과이기 때문에 경고하지 않습니다. 29 | 30 | **사용자들의 중요한 action이나 task가 완료되었다는 사실에 대한 확신을 줘야 합니다.** 예를 들어, 애플 페이 통신을 성공했다는 피드백을 주는 상황입니다. 사용자들이 자신의 action이나 task가 성공하기를 바라고 있기 때문에, 그들은 실패할 경우가 언제인지 아는 것이 필요합니다. 31 | 32 | **사용자들에게 실패의 원인과 이해를 돕도록 보여줘야 합니다.** 예를 들어, 사용자가 정확한 목적지가 없이 방향을 요청한다면, 지도는 목직지와 출발점이 같기 때문에 방향을 제공할 수 없다고 보여줘야 합니다. 33 | 34 | 35 | 36 |
37 | 38 | 👉🏻 [HIG의 Feedback의 일종 중 하나인 Toast가 왜 HIG Components에는 없을까?](https://www.wodyd.com/ios-hig-toast/)에 대한 고민을 글 39 | 40 |
41 | 42 | - https://developer.apple.com/design/human-interface-guidelines/patterns/feedback/ 43 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/CounterViewReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CounterViewReactor.swift 3 | // ReactorKitSample 4 | // 5 | // Created by Woody on 2022/10/02. 6 | // 7 | 8 | import Foundation 9 | import ReactorKit 10 | 11 | class CounterViewReactor: Reactor { 12 | 13 | enum Action { 14 | case plus 15 | case minus 16 | } 17 | 18 | enum Mutation { 19 | case increaseValue 20 | case decreaseValue 21 | case setLoading(Bool) 22 | case setAlertMessage(String) 23 | } 24 | 25 | struct State { 26 | var number: Int 27 | var isLoading: Bool 28 | 29 | @Pulse var alertMessage: String? 30 | } 31 | 32 | let initialState: State 33 | 34 | init() { 35 | self.initialState = State( 36 | number: 0, 37 | isLoading: false 38 | ) 39 | } 40 | 41 | func mutate(action: Action) -> Observable { 42 | switch action { 43 | case .plus: 44 | return Observable.concat([ 45 | .just(Mutation.setLoading(true)), 46 | .just(Mutation.increaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance), 47 | .just(Mutation.setLoading(false)), 48 | .just(Mutation.setAlertMessage("increased!")) 49 | ]) 50 | case .minus: 51 | return Observable.concat([ 52 | .just(Mutation.setLoading(true)), 53 | .just(Mutation.decreaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance), 54 | .just(Mutation.setLoading(false)), 55 | .just(Mutation.setAlertMessage("decreased!")) 56 | ]) 57 | } 58 | } 59 | 60 | func reduce(state: State, mutation: Mutation) -> State { 61 | var state = state 62 | switch mutation { 63 | case .increaseValue: 64 | state.number += 1 65 | case .decreaseValue: 66 | state.number -= 1 67 | case .setLoading(let isLoading): 68 | state.isLoading = isLoading 69 | case .setAlertMessage(let message): 70 | state.alertMessage = message 71 | } 72 | 73 | return state 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ReactorKitSample 4 | // 5 | // Created by Woody on 2022/08/04. 6 | // 7 | 8 | import UIKit 9 | import ReactorKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class ViewController: UIViewController, View { 14 | 15 | @IBOutlet weak var loadingIndicator: UIActivityIndicatorView! 16 | @IBOutlet weak var minusButton: UIButton! 17 | @IBOutlet weak var numberLabel: UILabel! 18 | @IBOutlet weak var plusButton: UIButton! 19 | 20 | var disposeBag = DisposeBag() 21 | let counterViewReactor: CounterViewReactor = CounterViewReactor() 22 | 23 | override func viewDidLoad() { 24 | bind(reactor: counterViewReactor) 25 | } 26 | 27 | func bind(reactor: CounterViewReactor) { 28 | plusButton.rx.tap 29 | .map { CounterViewReactor.Action.plus } 30 | .bind(to: reactor.action) 31 | .disposed(by: disposeBag) 32 | 33 | minusButton.rx.tap 34 | .map { CounterViewReactor.Action.minus } 35 | .bind(to: reactor.action) 36 | .disposed(by: disposeBag) 37 | 38 | reactor.state 39 | .map { "\($0.number)" } 40 | .distinctUntilChanged() 41 | .bind(to: numberLabel.rx.text) 42 | .disposed(by: disposeBag) 43 | 44 | reactor.state 45 | .map { $0.isLoading } 46 | .distinctUntilChanged() 47 | .bind(to: loadingIndicator.rx.isAnimating) 48 | .disposed(by: disposeBag) 49 | 50 | reactor.state 51 | .map { !$0.isLoading } 52 | .distinctUntilChanged() 53 | .bind(to: loadingIndicator.rx.isHidden) 54 | .disposed(by: disposeBag) 55 | 56 | reactor.pulse(\.$alertMessage) 57 | .compactMap { $0 } 58 | .subscribe(onNext: { [weak self] message in 59 | let alertViewController = UIAlertController( 60 | title: nil, 61 | message: message, 62 | preferredStyle: .alert 63 | ) 64 | alertViewController.addAction(UIAlertAction( 65 | title: "OK", 66 | style: .default 67 | )) 68 | self?.present(alertViewController, animated: true) 69 | }) 70 | .disposed(by: disposeBag) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CustomButton 4 | // 5 | // Created by Woody on 2022/07/06. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:07_Coding_Test.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.07 (금) 6 | 7 |
8 | 9 | 내일 카카오 2차 코딩테스트를 위해 [2022 카카오 블라인드 2차 과제](https://programmers.co.kr/app/with_setting/tests/34404/challenges)를 풀었습니다. 10 | 11 | API 통신을 통해 게임유저 MMR을 조절하여 비슷한 실력의 유저끼리 매칭시키는 알고리즘을 구현하는 문제입니다. 작년도에 풀었을 당시엔, 환경을 일반 Command Line Tool을 이용했기 때문에 세마포어를 이용해서 콜백 지옥을 해결했습니다. 하지만 이번엔 iOS Application으로 구현하여 async await을 이용한 통신으로 구현했습니다. parser도 라이브러리를 사용할 수는 있지만 코테 때 어떤 문제가 나올지 모르기때문에 라이브러리는 따로 사용하지 않았습니다. 12 | 13 | ---- 14 | 15 | 시나리오 1의 문제 점수에 영향을 미치는 것은 총 세개입니다. MMR이 비슷한 유저끼리 매칭되기, 유저의 MMR이 고유 실력 순위와 일치하게 만들기, 그리고 매칭을 신청한 유저가 기다린 시간을 최소화하기. 가장 먼저 접근한 방식은 그냥 구현하기 였습니다. 먼저 유저들의 MMR을 모두 5000으로 초기화해주었습니다. 그리고 대기 유저 리스트에 들어오는 순서대로 매칭시키고 대결 결과에서 이긴 유저는 1을 더하고 진 유저는 1을 뺐습니다. 간단한 구현을 통해서 186.8761점을 획득할 수 있습니다. 16 | 17 | 결과 1 18 | 19 | 일반적으로 점수를 높일 수 있는 방법은 비슷한 MMR을 가진 유저끼리 매칭하는 것입니다. 대기 유저가 있는 경우 유저의 MMR 정보를 가져와 차이가 가장 적은 MMR을 가진 유저끼리 매칭하면 198.7717점을 획득할 수 있습니다. 정말 미미한 점수차이지만 12점을 올릴 수 있습니다. 20 | 21 | 결과 2 22 | 23 | 다음 생각할 수 있는 방법은 대결 결과에서 승자와 패자에게 더하고 뺴는 가중치를 조절하는 방식입니다. 문제에서 대결시간과 승자와 패자의 MMR 차이의 관계가 주어져있습니다. 대결시간이 클 수록 MMR이 비슷한 유저들의 대결이고 대결시간이 작을 수록 MMR 차이가 큰 유저들의 대결입니다. 따라서, (40 - 대결 시간) * E를 가중치로 계산합니다. (40은 대결의 최대 시간이 40입니다.) 여기서 E가 관건이었는데 처음엔 2.5로 계산했더니 220.6579점을 획득했습니다. 하지만 35.5로 계산하 235.9379점을 획득할 수 있습니다. E의 값을 정확히 어떻게 구하는지는 모르겠지만 조절하며 알고리즘을 수정했습니다. 24 | 25 | 결과 4 26 | 27 | 유저가 대기하는 시간은 고려를 하지 않았습니다. 아니. 고려를 하긴 했습니다. 10분 대기를 한 유저만 매칭을 했더니 점수가 178점으로 줄었습니다. 그래서 대기 리스트에 올라오는 유저를 바로바로 매칭시켰습니다. 28 | 29 | 시나리오 2의 어뷰저 처리는 대결 시간이 10분 이하인 유저들의 대결을 전부 배제시켰습니다. 이 문제는 시뮬레이션을 한번 돌리는데 너무 오래 걸려 끝까지 돌려보진 못했습니다만 정확도가 조금 높아졌을 거라 예상합니다. 30 | 31 | 문제 2 -결과 1 32 | 33 | 문제를 다 풀고 [안즈님](https://anz1217.tistory.com/140?category=799868)의 블로그를 읽고 가중치 조절에 대해 더 아이디어를 얻을 수 있었습니다. A와 B의 대결에서 A가 승리했다고 해보면, (기존 A의 MMR - 기존 B의 MMR)이 작을수록 가중치 변동을 크게 합니다. 더 실력이 높은 상대일수록 이를 이긴 유저의 MMR은 높을 가능성이 크고, 반대로 실력이 낮은 상대일수록 이에 진 유저의 MMR은 낮을 가능성이 크기 떄문입니다. 또 한가지 진행된 게임 시간이 짧을수록 가중치를 더 커지도록 합니다. 게임 시간이 짧을수록 두 유저의 실력차가 클 가능성이 크기 때문입니다. 가중치를 매 경기 결과마다 조절하면서 풀면 점수가 더 높게 나올 것이라고 생각합니다. 😓 34 | 35 | ---- 36 | 37 | 총 414점을 획득했네요. ㅠ 더 높이고 싶습니다. 내일도 화이팅! -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:19_explore_ui_animation_hitches_and_the_render_loop.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.19 (수) 6 | 7 | 8 | 9 | ### Hitch? 10 | 11 | Hitch는 예상했던 것보다 화면이 더 늦게 나오는 현상입니다. 스크롤할 때 버벅이는 현상을 Hitch라고 합니다. 스크롤을 할 때, 손가락을 계속 올리긴 하지만, 다음 콘텐츠가 보이지 않고 이전 콘텐츠가 보이면 Hitch가 발생했다고 합니다. Hitch는 Render Loop가 프레임 계산을 제시간안에 실패하면 나타납니다. 12 | 13 | Render Loop는 유저가 터치하고 나서부터 OS가 프레임을 연산하고 UI를 바꿀때까지의 과정을 말합니다. 이 과정은 게속 반복되는 과정이기에 루프라고 합니다. 아이폰은 초에 60프레임입니다. (1프레임은 16.67ms임) 아이패드 프로는 120프레임입니다. 14 | 15 | 프레임의 시작점마다 디바이스는 VSYNC라는 이벤트를 방출하는데 VSYNC(브이싱크)는 새 프레임이 준비되어야 하는 시점입니다. 이 VSYNC가 방출되는 시점은 디바이스마다 다릅니다. 16 | 17 | 프레임이 준비되는 과정은 총 세단계입니다. 첫 번째는 App에서 이벤트가 처리되어 UI 변경사항을 결정하는 단계입니다. 두 번째는 Render Server에서 실제로 렌더링되는 단계, 그리고 세 번째는 프레임을 표시하는 단께입니다. 18 | 19 | > App에서 UI 변경 결정 -> Render Server에서 렌더링 -> 프레임 표시 (Ui 표시) 20 | 21 | App과 Render Server에서 각각 1프레임을 잡아먹어, UI에 프레임을 표시하기 전에 총 2프레임을 잡아먹는 이중 버퍼링이 되지만 다른 방법으론 Render Server에서 렌더링하는데 1프레임을 추가 제공되기도 합니다.(Falling mode) 22 | 23 | Render Loop는 총 5단계로 이루어졌습니다. 24 | 25 | > Event -> Commit -> Render prepare -> Render execute -> Display 26 | 27 | Event : 유저가 터치하는 단계, UI를 변경할건가 결정 단계 28 | Commit : 앱이 UI를 변경을 요청하고 render server에 전달하는 단계 29 | Render prepare : commit 단계에서 전달받은 변경 사항을 GPU에서 그릴 수 있도록 준비하는 단계 30 | Render execute : UI를 그리는 단계 31 | Display : 사용자에게 프레임을 보여주는 단계 32 | 33 | 34 | 35 | ![event_phase](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/event_phase.png) 36 | 37 | 예시로 사용자가 bound를 변경시키는 이벤트를 발생시켰다고 해봅시다. 38 | Event단계에서는 Core Animation은 다시 계산해야 하는 하위 뷰의 레이아웃을 식별하기 위해 `setNeedLayout`을 호출합니다. 39 | Commit단계에서는 다시 그려야할 layout을 다음 업데이트 주기에 반영하도록 `setNeedsDisplay`를 호출합니다. 40 | Render prepare단계에서는 commit단계에서 전달받은 변경사항을 그립니다. 이 때 렌더링 순서는 위에서부터 아래 순서로 진행됩니다. 41 | Render execute단계에서는 Prepare Phase에서 준비해준대로 그려주는 작업을 합니다. 42 | 마지막 display 단계에서는 렌더링 결과를 보여주기만 하면 됩니다. 43 | 44 | 하나의 프레임을 완성하는 각 과정은 병렬적으로 일어나는데 하나의 파이프라인이라도 다음 VSYNC가 발생하기 전에 처리되지 못하면 병목현상이 발생합니다. 즉, 다음 VSYNC가 발생하기까지 처리를 못하면 hitch가 발생합니다. 45 | 46 | ![def](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/def.png) 47 | 48 | ### Hitch 종류 49 | 50 | - Commit 51 | - Render 52 | 53 | 54 | 55 | ![dfasd](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/dfasd.png) 56 | 57 | 단계 하나가 자신의 VSYNC 주기에 처리되지 않고 다음 VSYNC에 마무리가 되면 다음 단계들도 줄줄이 밀립니다. 위 사진에선 App에서 event단계가 밀렸기때문에, commit단계도 다음 VSYNC에 처리가 되는 상황을 볼 수 있습니다. 58 | 59 | 60 | 61 | ### Measuring hitch time 62 | 63 | ![dsakjbfsad](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/dsakjbfsad.png) 64 | 65 | 하나의 프레임만 만들 때 Hitch가 발생한 정도를 측정하긴 쉽지만 스크롤이나 애니메이션 등의 이벤트에서 hitch가 일어나고 모든 이벤트가 render server로 UI변경사항을 전달하는 게 아니라 정확히 측정하긴 어렵습니다. 66 | 67 | > **대신 hitch가 발생한 시간을 총 시간으로 나눠서 계산하는 방법을 사용합니다. -> hitch time ratio** 68 | 69 | ![vdfdj](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/vdfdj.png) 70 | 71 | 30프레임이 모두 자신의 VSYNC 안에서 해결된다면 hitch는 발생하지 않고 당연히 hitch time ratio도 0이 됩니다. 72 | 73 | ![udf](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/udf.png) 74 | 75 | 만약, 몇군데 밀리게 된다면 hitch time / duration 을 통해 hitch time ratio를 측정할 수 있습니다. 76 | 77 | > hitch time ratio = hitch time / duration 78 | 79 | 80 | 81 | ![sinho](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/sinho.png) 82 | 83 | 애플은 이 hitch time ratio에 색깔도 부여했습니다. 이 hitch time ratio가 노란색이면 고쳐야합니다. 84 | 85 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/PulseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulseViewController.swift 3 | // ReactorKitSample 4 | // 5 | // Created by Woody on 2022/10/02. 6 | // 7 | 8 | import UIKit 9 | import ReactorKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | class PulseViewController: UIViewController, View { 14 | 15 | typealias Reactor = PulseReactor 16 | 17 | lazy var stateButton: UIButton = { 18 | let v = UIButton(type: .system) 19 | v.translatesAutoresizingMaskIntoConstraints = false 20 | v.setTitle("상태 테스트", for: .normal) 21 | v.setTitleColor(.blue, for: .normal) 22 | return v 23 | }() 24 | 25 | lazy var pulseButton: UIButton = { 26 | let v = UIButton(type: .system) 27 | v.translatesAutoresizingMaskIntoConstraints = false 28 | v.setTitle("펄스 테스트", for: .normal) 29 | v.setTitleColor(.blue, for: .normal) 30 | return v 31 | }() 32 | var disposeBag: DisposeBag = DisposeBag() 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | view.backgroundColor = .white 37 | view.addSubview(stateButton) 38 | view.addSubview(pulseButton) 39 | 40 | NSLayoutConstraint.activate([ 41 | stateButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), 42 | stateButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 100), 43 | 44 | pulseButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), 45 | pulseButton.topAnchor.constraint(equalTo: self.stateButton.bottomAnchor, constant: 100), 46 | ]) 47 | } 48 | 49 | func bind(reactor: Reactor) { 50 | 51 | stateButton.rx.tap.map { .didTapStateButton } 52 | .bind(to: reactor.action) 53 | .disposed(by: disposeBag) 54 | 55 | pulseButton.rx.tap.map { .didTapPulseButton } 56 | .bind(to: reactor.action) 57 | .disposed(by: disposeBag) 58 | 59 | reactor.state 60 | .map { "\($0.value)" } 61 | .distinctUntilChanged() 62 | .subscribe(onNext: { 63 | print("👋🏻👋🏻👋🏻 상태 1") 64 | }).disposed(by: disposeBag) 65 | 66 | reactor.state 67 | .map { $0.message } 68 | .subscribe(onNext: { message in 69 | print("🚀🚀🚀 상태 2") 70 | }).disposed(by: disposeBag) 71 | 72 | reactor.pulse(\.$error) 73 | .observe(on: MainScheduler.instance) 74 | .subscribe(onNext: { _ in 75 | print("✨✨✨ 펄스 ") 76 | }).disposed(by: disposeBag) 77 | } 78 | } 79 | 80 | class PulseReactor: Reactor { 81 | 82 | enum Action { 83 | case didTapStateButton 84 | case didTapPulseButton 85 | } 86 | 87 | struct State { 88 | var message: String = "" 89 | var value: String = "" 90 | 91 | @Pulse var error: String? 92 | } 93 | 94 | var initialState: State = State() 95 | 96 | func reduce(state: State, mutation: Action) -> State { 97 | var newState = state 98 | switch mutation { 99 | case .didTapStateButton: 100 | newState.message = "메세지" 101 | case .didTapPulseButton: 102 | newState.error = "펄스" 103 | } 104 | return newState 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,xcode,swift 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,xcode,swift 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Swift ### 38 | # Xcode 39 | # 40 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 41 | 42 | ## User settings 43 | xcuserdata/ 44 | 45 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 46 | *.xcscmblueprint 47 | *.xccheckout 48 | 49 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 50 | build/ 51 | DerivedData/ 52 | *.moved-aside 53 | *.pbxuser 54 | !default.pbxuser 55 | *.mode1v3 56 | !default.mode1v3 57 | *.mode2v3 58 | !default.mode2v3 59 | *.perspectivev3 60 | !default.perspectivev3 61 | 62 | ## Obj-C/Swift specific 63 | *.hmap 64 | 65 | ## App packaging 66 | *.ipa 67 | *.dSYM.zip 68 | *.dSYM 69 | 70 | ## Playgrounds 71 | timeline.xctimeline 72 | playground.xcworkspace 73 | 74 | # Swift Package Manager 75 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 76 | # Packages/ 77 | # Package.pins 78 | # Package.resolved 79 | # *.xcodeproj 80 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 81 | # hence it is not needed unless you have added a package configuration file to your project 82 | # .swiftpm 83 | 84 | .build/ 85 | 86 | # CocoaPods 87 | # We recommend against adding the Pods directory to your .gitignore. However 88 | # you should judge for yourself, the pros and cons are mentioned at: 89 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 90 | # Pods/ 91 | # Add this line if you want to avoid checking in source code from the Xcode workspace 92 | # *.xcworkspace 93 | 94 | # Carthage 95 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 96 | # Carthage/Checkouts 97 | 98 | Carthage/Build/ 99 | 100 | # Accio dependency management 101 | Dependencies/ 102 | .accio/ 103 | 104 | # fastlane 105 | # It is recommended to not store the screenshots in the git repo. 106 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 107 | # For more information about the recommended setup visit: 108 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 109 | 110 | fastlane/report.xml 111 | fastlane/Preview.html 112 | fastlane/screenshots/**/*.png 113 | fastlane/test_output 114 | 115 | # Code Injection 116 | # After new code Injection tools there's a generated folder /iOSInjectionProject 117 | # https://github.com/johnno1962/injectionforxcode 118 | 119 | iOSInjectionProject/ 120 | 121 | ### Xcode ### 122 | 123 | ## Xcode 8 and earlier 124 | 125 | ### Xcode Patch ### 126 | *.xcodeproj/* 127 | !*.xcodeproj/project.pbxproj 128 | !*.xcodeproj/xcshareddata/ 129 | !*.xcworkspace/contents.xcworkspacedata 130 | /*.gcno 131 | **/xcshareddata/WorkspaceSettings.xcsettings 132 | 133 | # End of https://www.toptal.com/developers/gitignore/api/macos,xcode,swift 134 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:22_making_core_animation.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.22 (토) 6 | 7 | 8 | 9 | 오늘의 애니메이션은 아직 성공하지 못했습니다. ㅠ 거의 80%가량 성공한 것 같은데 매끄럽게 이어지지가 않아서 좀 더 손봐야할 것 같아요. 공부한 내용은 [블로그](https://wodyios.tistory.com/72?category=897235)에 작성했습니다. (연습이었을 뿐 진짜 구현할 애니메이션은 아니에요.) 10 | 11 | `KeyFrameAnimation` 클래스의 프로퍼티들을 살펴보게 되었습니다. `fillMode` / `keyTimes` / `timeOFfset` 등등.. 애니메이션 시간과 관련된 프로퍼티들이 많아서 Core Animation의 시간과 관련된 문서를 읽어봤습니다. 근데 이 내용이 조금 어렵고 복잡해서 전부는 이해하진 못했지만 내일도 읽어봐서 되도록 다 이해해야 할 것 같습니당 12 | 13 | ---- 14 | 15 | 16 | 17 | ### KeyframeAnimation 18 | 19 | - `values` : An array of objects that specify the keyframe values to use for the animation. 20 | 애니메이션에 사용될 keyframe 값들을 지정해줍니다. `CAKeyframeAnimation`에서 애니메이션을 주기 위해서 꼭 필요한 값입니다. 이 값은 `path` 프로퍼티가 nil일 때만 사용가능합니다! 동시에 사용하면 X. 21 | - `keyTimes` : An optional array of `NSNumber` objects that define the time at which to apply a given keyframe segment. 22 | 0~1 사이의 숫자로 이루어진 배열입니다. 위에서 준 values를 언제 애니메이션을 줄 지 타이밍과 관련된 요소입니다. `values`의 배열과 같은 개수를 가지거나 `path` 프로퍼티의 control point와 개수가 같아야 의도대로 동작합니다. 23 | 이 값은 `calucationMode` 프로퍼티와 연관이 깊습니다. `calucationMode`가 linear, cubic이라면, 위의 규칙과 일치해야합니다. (개수가 같아야함. 처음과 마지막 값은 0과 1) 만약 discrete이라면 개수보다 하나가 더 많아야합니다. cubicPaced 또는 paced라면 이 프로피터는 무시됩니다. 이 규칙을 지키지 않을 경우에도 이 프로퍼티는 무시됩니다. 24 | 25 | - `fillMode` : Determines if the receiver’s presentation is frozen or removed once its active duration has completed. 26 | 애니메이션의 움직임이 끝났을 때, 해당 레이어의 프레젠테이션, 즉 보여지는 것은 멈추거나 혹은 제거되거나 혹은 남아있거나를 정해주는 프로퍼티입니다. backwards, forwards, removed, both가 있는데 이것도 CAMediaTiming 프로토콜과 관련이 있습니다. 27 | 28 | > CAMediaTiming 프로토콜은 애니메이션을 주는데 정말 중요한 요소구나 29 | 30 | - `isRemovedOnCompletion` : Determines if the animation is removed from the target layer’s animations upon completion. 31 | 애니메이션의 움직임이 끝났을 때 해당 애니메이션을 레이어에서 제거할 것인지 말것인지에 대한 여부입니다. 32 | 33 | > 애니메이션이 끝나고 해당 위치를 유지하고 싶은 경우 애니메이션을 지우지않고(isRemovedOnCompletion = false), fillMode를 forwards로 설정하면 된다. 34 | 35 | - `isAdditive` : Determines if the value specified by the animation is added to the current render tree value to produce the new render tree value. 36 | 현재 위치 기준에서 애니메이션 할 건지 안할 건지에 대한 여부입니다. 현재 레이어 렌더 트리에 새로운 렌더트리 값을 생성해서 추가합니다. 37 | 38 | - `timeOffset` : Specifies an additional time offset in active local time. 39 | 현재 애니메이션이 실행되고 있는 시간에 추가 시간을 더하는 것입니다. 만약 position을 0에서 10으로 이동시키는 애니메이션이 있다고 가정하면 timeOffset이 4초라고 하면 4 -> 10 -> 0 -> 4로 이동합니다. 즉, 4에서부터 애니메이션이 시작합니다. 40 | 41 | - `beginTime` : Specifies the begin time of the receiver in relation to its parent object, if applicable. 42 | 43 | 애니메이션 시작 시간 44 | 45 | ### CAMediaTiming 46 | 47 | 절대적인 시간은 단위 "초"로 계산됩니다. [`CACurrentMediaTime()`](https://developer.apple.com/documentation/quartzcore/1395996-cacurrentmediatime)는 절대적인 현재 시각을 쉽게 가져올 수 있습니다. 부모의 시간에서 지역 시간 (from parent time to local tiem)으로 변환하는 과정은 두 단계가 있습니다. 48 | 49 | 1. "active local time"으로 변환 50 | 2. "active local time"에서 "basic local time"으로 변환 51 | 52 | 1번 단계에서는 현재 객체가 부모 객체의 시간 속에서 어디에 나타날 지 포인트를 알 수 있습니다. 그리고 두번째 단계에서는 타이밍 모델은 해당 애니메이션을 반복할 수 있고, 뒤로 되돌릴 수 있도록 도와줍니다. 53 | 54 | > 여기까지 문서를 읽어보면 정확하게 어떤 건지 감이 오지는 않지만, 대략적으로 어떻게 작동할 건지는 감이 옵니다. 55 | > Timing 또한 뷰계층처럼 계층으로 이루어져있고 frame과 bound처럼 부모와의 상대적인 시간과 나의 절대적인 시간이 있다고 이해할 수 있습니다. 56 | > 그래서 [Customizing the Timing of an Animation](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1) 문서를 읽어봤습니다. 57 | 58 | 위에서 이해한대로, 각 레이어는 각자의 로컬 시간을 가지고 있고, 각각의 시간 차이는 유저가 차이를 느끼지 못할 정도로 작습니다. 만약, 레이어의 [속도](https://developer.apple.com/documentation/quartzcore/camediatiming/1427647-speed)를 변경하게 된다면 애니메이션의 duration도 변경될 것이고 다른 레이어와 다르게 작동하겠죠. 59 | 60 | > 만일 속도=0이면? 그냥 애니메이션이 작동안할 것 같습니다. 61 | 62 | CAKeyframeAnimation 문서에서 `beginTime` 프로퍼티에 대한 설명을 읽었을 때는 잘 이해가 되지 않았었는데.. 여기서도 설명이 나와있습니다. `beginTime` 프로퍼티는 애니메이션을 시작 시각을 지정하기 위해서 사용합니다. 애니메이션 두가지를 연달아 이용하기 위해서는 한개의 애니메이션이 끝나는 시간을 다른 하나의 애니메이션의 시작시간으로 설정하면 됩니다. 결국 beginTime도 절대시간인 것 같습니다. -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:12_dynamic_tableviewcell_of_tableview_in_scrollview_height_not_worked.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.12 (수) 6 | 7 |
8 | 9 | - [이슈 46번](https://github.com/wody-d/woody-iOS-tip/issues/46) 10 | 11 | 12 | 13 | ## 🚨 문제상황 14 | 15 | 개발을 하다가 이상한 UI 버그를 만났습니다.
scrollView 안에 tableView가 위치해있는 UI입니다. 스크롤은 scrollView만 되고 tableView의 `isScrollEnabled` 프로퍼티는 false로 설정했습니다. 이 때 tableView의 콘텐츠에 따라 스크롤뷰의 contentSize가 변해야하는데 tableView의 datasource에 데이터가 제대로 configure됐음에도 불구하고 contentSize가 늘어나지 않고 tableView의 높이가 제멋대로 설정되는 이슈를 만났습니다. 16 | 17 | > 상황 설명이 이해되도록 스토리보드의 hierachy를 그려보았습니다.
스크롤뷰 안에 StackView를 삽입했고 상하좌우 constriant를 잡아주었고 tableView의 height constraint를 잡은 상황입니다. 18 | 19 |
20 | 21 | 위와 같은 구조에서는 tableView가 리로드 이벤트 호출 시 레이아웃 업데이트를 처리하면 됩니다. 22 | 23 | ```swift 24 | weak var tableViewheightConstraint: NSLayoutConstraint! 25 | 26 | self.tableView.reloadData() 27 | self.tableViewheightConstraint.constant = self.commentHight * CGFloat(commentCount) 28 | self.view.layoutIfNeeded() 29 | ``` 30 | 31 | 혹은, 32 | 33 | ```swift 34 | override func viewDidLayoutSubviews() { 35 | super.viewDidLayoutSubviews() 36 | 37 | self.tableViewheightConstraint.constant = self.tableView.contentSize.height 38 | self.view.layoutIfNeeded() 39 | } 40 | ``` 41 | 42 | 에서 처리해줍니다. [예전에도 한번 겪었던 트러블 슈팅](https://github.com/wody-d/woody-iOS-tip/issues/1)이었습니다. 43 | 44 | ```swift 45 | // 만약 collectionView라면, collectionViewLayout의 collectionViewContentSize 프로퍼티의 height 할당 46 | collectionView.reloadData() 47 | collectionViewHeightConstraint.constant = collectionView.collectionViewLayout.collectionViewContentSize.height 48 | view.layoutIfNeeded() 49 | ``` 50 | 51 | 52 | 53 | 예전에 만난 이슈는 Cell의 높이가 정적이었지만 이번엔 동적인 경우입니다. 아래와 같이 Label과 버튼으로 이루어져있는 Cell이고 Label의 콘텐츠의 길이가 정해져 있지 않아 무한으로 늘어날수 있습니다. (`numberOfLines = 0`) 54 | 55 |
56 | 57 | ## 원인 58 | 59 | 테이블뷰 reload 이벤트 호출 시 셀은 자신의 높이를 파악합니다. 하지만 동적으로 높이가 정해지는 셀이라면 연산이 될 때까지 셀의 높이를 알 수 없습니다. 따라서 매번 `contentSize`의 높이가 달라지고 처음에 한번에 정할 수 없습니다. 60 | 61 | ## 해결 방법 62 | 63 | 해결하는 방법으로는 여러가지가 있습니다. 먼저, 테이블뷰 셀의 높이를 지정해주면 됩니다. Cell의 높이 * Cell의 개수는 결국 contentSize의 높이가 됩니다. 하지만 이것은 동적인 높이의 셀을 만들 때는 사용하지 못합니다. 64 | 65 | ```swift 66 | let commentHight: CGFloat = 88.0 67 | let commentCount = commentModels.count 68 | self.tableViewheightConstraint.constant = self.commentHight * CGFloat(commentCount) 69 | ``` 70 | 71 | 두 번째로는, Cell의 높이를 연산해줍니다. 이미 데이터를 모두 가져왔기 때문에 해당 데이터가 UI에 그려진다 가정하고 높이를 계산하는 방법입니다. 위에서 나온 이슈 상황에서는 `UILabel`의 높이를 text Size에 맞추어야 합니다. 이 때 아래처럼 `intrinsicContentSize`를 이용해서 실제 label의 width가 뷰의 width보다 몇 배 긴가에 따라 높이를 곱해주는 방법을 이용할 수 있습니다. . 72 | 73 | ```swift 74 | let label = UILabel() 75 | label.numberOfLines = 0 76 | label.lineBreakMode = .byCharWrapping 77 | label.text = "iOS 개발자 이재용! 삼성 회장님과 이름이 같다고 안드로이드를 개발하지 않아요~ 오해말아요." 78 | 79 | let newSize = label.intrinsicContentSize 80 | label.frame.size = newSize 81 | 82 | if label.frame.width > label.frame.width { 83 | let shareOfDivision = ceil((testLabel.frame.width / view.frame.width)) 84 | let newHeight = label.frame.height * shareOfDivision 85 | label.frame.size = CGSize(width: view.frame.width, height: newHeight) 86 | } 87 | 88 | print(label.frame.size.height) 89 | ``` 90 | 91 | 각 Cell마다 연산하고 최종 나온 높이를 할당하면 됩니다. 92 | 93 | ```swift 94 | self.tableViewheightConstraint.constant = totalHeight 95 | ``` 96 | 97 | 세 번째론, UI 구조를 완전히 바꾸는 방법이 있습니다. 테이블뷰가 스크롤뷰안에 있는 구조에서 only 테이블뷰만 있는 구조로 바꾸면 해당 이슈는 사라집니다. 하지만, 이 방법은 추천하지 않습니다. 테이블 뷰가 아닌 컴포넌트들도 모두 리로드가 되는데 이 상황은 좋지 않습니다. 저도 예전에 사용해봤지만 리로드 이벤트는 모든 아이템을 리로드되고 보여지고 있는 아이템 셀들이 다시 그려지기 때문에 깜박일 수 있습니다. (애니메이션도 이쁘게 되지 않습니다.) 사실 디퍼블 데이터 소스를 이용하는 것도 하나의 방법입니다만, 디퍼블 데이터소스는 아직까지 많이 익숙치 못해 개발을 빠르게 해야했던 오늘 상황과는 맞지 않았습니다. 디퍼블 데이터소스는 아직 새로 생기거나 수정된 아이템을 재 apply하고 dataSource에서 itemIdentifier를 꺼내는 과정이 익숙하지 않았습니다. ~~더욱 훈련해야겠다~~ 98 | 99 | 너무 오랜만에 테이블뷰 In 스크롤뷰 구조를 그리다보니 생각치못한 트러블을 만나서 세시간정도 고생했습니다. 마음도 급했어서 해결과정이 빠르게 생각이 나지 않았습니다. 그래도 이슈을 해결했을 때는 기분이 뿌듯합니다. 100 | 101 | > 이쁘진 않지만 이슈 해결한 UI 102 | 103 |
-------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:16_button_configuration_more.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.16 (일) 6 | 7 | 8 | 9 | UIButton.Configuration을 조금 더 깊게 파보겠습니다. 10 | 11 | ### 기본적인 요소 12 | 13 | ```swift 14 | // 선언 15 | var configuration = UIButton.Configuration.filled() 16 | 17 | // 표시되는 UI 18 | configuration.title = "버튼입니다" 19 | configuration.subtitle = "서브 타이틀입니다" 20 | configuration.image = .init(systemName: "xmark") 21 | 22 | // UI의 간격 및 위치, 사이즈 조절 23 | configuration.titleAlignment = .center 24 | configuration.titlePadding = 10 25 | configuration.imagePlacement = .trailing 26 | configuration.imagePadding = 10 27 | configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(scale: .medium) 28 | 29 | // 사이즈 30 | configuration.buttonSize = .mini 31 | configuration.buttonSize = .small 32 | configuration.buttonSize = .medium 33 | configuration.buttonSize = .large 34 | ``` 35 | 36 | > 사이즈가 있지만 Constriaint를 지정하면 이 값은 무시됩니다. 왜 있는 지 모르겠습니다. 단지 개발자가 ""이 버튼의 사이즈는 이정도이다."라고 명세하는 용도로만 사용되는 것 같습니다. 37 | > [아래 영어는 문서에 작성된 buttonSize의 Discussion](https://developer.apple.com/documentation/uikit/uibutton/configuration/3750783-buttonsize): 38 | > The size indicates a system-defined size you prefer for this button. The exact size of the button may change regardless of this value. 39 | 40 | ```swift 41 | configuration.cornerStyle = .small 42 | configuration.cornerStyle = .medium 43 | configuration.cornerStyle = .large 44 | configuration.cornerStyle = .capsule 45 | configuration.cornerStyle = .dynamic 46 | ``` 47 | 48 | `cornerStyle`은 background corner radius를 무시하고 버튼 corner radius를 설정하는 프로퍼티입니다. 여기서 `fixed` 케이스만 혼자만 background corner radius를 따릅니다. 49 | 50 | > 그런데, dynamic과 fixed의 차이가 뭘까? 이건 쫌 나중에 찾아봅시다. 51 | 52 | ```swift 53 | configuration.cornerStyle = .fixed 54 | configuration.background.cornerRadius = 14 55 | ``` 56 | 57 | ---- 58 | 59 | ### 조금 깊게 - 버튼 제목 프로퍼티 수정하기 60 | 61 | ```swift 62 | configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in 63 | var outgoing = incoming 64 | outgoing.font = UIFont.preferredFont(forTextStyle: .headline) 65 | return outgoing 66 | } 67 | ``` 68 | 69 | incoming과 outgoing은 `AttributeContainer` 타입입니다. title과 관련된 attribute를 수정하고 반환합니다. 70 | 71 | > AttributeContainer도 나중에 공부합시다. 일단은 UILabel의 여러 프로퍼티들을 가지고 있는 타입이라고만 알아둡시다. 72 | 73 | --- 74 | 75 | ### 더욱 깊게 - 로딩 버튼 76 | 77 | ```swift 78 | button.configurationUpdateHandler = { [unowned self] button in 79 | var config = button.configuration 80 | config?.showsActivityIndicator = self.signingIn 81 | config?.imagePlacement = self.signingIn ? .leading : .trailing 82 | config?.title = self.signingIn ? "Signing In..." : "Sign In" 83 | button.isEnabled = !self.signingIn 84 | button.configuration = config 85 | } 86 | 87 | // Configuration 업데이트 88 | button.setNeedsUpdateConfiguration() 89 | ``` 90 | 91 | 버튼에서 `showsActivityIndicator`프로퍼티를 통해 `Indicator`를 보여줄 수 있습니다. 해당 프로퍼티는 Configuration에 속해있고 보여주기 위해서는 버튼의 Configuration을 업데이트해야합니다. 이 때 `configurationUpdateHandler`프로퍼티를 사용합니다. Configuration 업데이트 클로저를 작성한 후 직접 업데이트를 하기 위해서는 `setNeedsUpdateConfiguration()`메소드를 호출합니다. 92 | 93 | > [unowned self]를 사용하는 이유에 대해서 한번 생각해봅시다. 94 | 95 | ---- 96 | 97 | ### 더욱 깊게 - 토글 버튼 98 | 99 | ```swift 100 | button.configurationUpdateHandler = { [unowned self] button in 101 | var config = button.configuration 102 | let symbolName = self.isBookInCart ? "cart.badge.minus" : "cart.badge.plus" 103 | config?.image = UIImage(systemName: symbolName) 104 | button.configuration = config 105 | } 106 | 107 | // Configuration 업데이트 108 | button.setNeedsUpdateConfiguration() 109 | ``` 110 | 111 | 토글 버튼은 로딩 버튼에서 이용했던 `configurationUpdateHandler`프로퍼티를 그대로 사용하면 됩니다. 112 | 113 | ---- 114 | 115 | ### 더욱 깊게 - 팝업 버튼 116 | 117 | ```swift 118 | button.showsMenuAsPrimaryAction = true 119 | button.menu = UIMenu(children: [ 120 | UIAction(title: "Action 1", image: UIImage(systemName: "hare.fill")) { _ in 121 | print("Action 1") 122 | }, 123 | UIAction(title: "Action 2", image: UIImage(systemName: "tortoise.fill")) { _ in 124 | print("Action 2") 125 | } 126 | ]) 127 | 128 | button.changesSelectionAsPrimaryAction = true 129 | ``` 130 | 131 | 팝업버튼은 `showsMenuAsPrimaryAction`프로퍼티를 true로 설정하고 `menu`에 `UIAction`을 담아줘야합니다. `changesSelectionAsPrimaryAction`을 true로 설정한다면 팝업이 나올 때 마지막에 선택한 Action이 선택이 된 상태로 나옵니다. (만일 처음이라면 첫번째 Action이 선택되어져있습니다.) 132 | 133 | ---- 134 | 135 | ### 더욱 깊게 - UIAction 136 | 137 | ```swift 138 | let button = UIButton(configuration: .filled(), primaryAction: UIAction(handler: { action in 139 | print("안녕") 140 | })) 141 | ``` 142 | 143 | UIAction은 메뉴의 하나의 기능으로, 클로저를 통해 액션을 실행합니다. 메뉴의 일종이지만 버튼의 initiailzer에도 설정할 수 있어서 `addTarget`대신 사용할 수도 있습니다. 하지만 버튼의 초기화이후에는 버튼에 따로 설정할 수 는 없습니다. 144 | 145 | 146 | 147 | 148 | 149 | ### UIButton Configuration Ref 150 | 151 | - https://www.raywenderlich.com/27854768-uibutton-configuration-tutorial-getting-started 152 | - https://github.com/wody-d/woody-iOS-tip/issues/22 153 | - https://developer.apple.com/documentation/uikit/uiaction -------------------------------------------------------------------------------- /TIL/2022/WWDC22_introduce_charts.md: -------------------------------------------------------------------------------- 1 | ### WWDC22 SwiftUI Charts 2 | 3 | ---- 4 | 5 | 2022.11.01 (화) 6 | 7 | - Charts 8 | - 여러가지 기능 제공 9 | 10 | ## Charts 11 | 12 | 여러가지 요소별 차트가 있습니다. 막대 차트를 예시로 학습해보겠습니다. 13 | 14 | Bar Marks는 막대를 이용하여 데이터의 각 항목을 표현합니다. 이 때 value 팩토리 메소드를 사용하고 막대의 위치나 높이를 직접 설정하지 않아도 됩니다. Swift Chart 프레임워크는 막대만 자동으로 생성하지 않고, X축 막대에 대한 레이블과 막대의 길이가 의미하는 Y축의 이름도 보여줍니다. 15 | 16 | ```swift 17 | struct ContentView: View { 18 | var body: some View { 19 | Chart { 20 | BarMark( 21 | x: .value("Name", "Cachapa"), 22 | y: .value("Sales", 916) 23 | ) 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | 30 | 31 | 여러개의 막대를 표현하기 위해서는 ForEach 구문을 이용할 수 있습니다. 32 | 33 | ```swift 34 | 35 | struct Pancakes: Identifiable { 36 | let name: String 37 | let sales: Int 38 | 39 | var id: String { name } 40 | } 41 | 42 | let sales: [Pancakes] = [ 43 | .init(name: "Canhapa", sales: 916), 44 | .init(name: "Injera", sales: 856), 45 | .init(name: "Crepe", sales: 802) 46 | 47 | ] 48 | struct ContentView: View { 49 | var body: some View { 50 | Chart { 51 | ForEach(sales) { element in 52 | BarMark( 53 | x: .value("Name", element.name), 54 | y: .value("Sales", element.sales) 55 | ) 56 | } 57 | } 58 | } 59 | } 60 | 61 | ``` 62 | 63 | 64 | 65 | 66 | 67 | 만일 ForEach가 차트에서 유일한 콘텐츠인 경우, 차트 이니셜라이저에 데이터를 직접 넣을 수 있습니다. 68 | 69 | ```swift 70 | struct ContentView: View { 71 | var body: some View { 72 | Chart(sales) { element in 73 | BarMark( 74 | x: .value("Name", element.name), 75 | y: .value("Sales", element.sales) 76 | ) 77 | } 78 | } 79 | } 80 | ``` 81 | 82 | 콘텐츠가 늘어남에 따라 막대의 레이블이 점점 가까워집니다. 이를 해결하기 위햇 X축과 Y축을 바꾸고 차트를 눕힐 수 있습니다. 83 | 84 | 85 | 86 | 다크모드, 기기에 따른 폰트 크기, 차트 크기, 기기가로모드 등 여러 접근성을 지원합니다. 또한, VoiceOver 기능도 지원합니다. VoiceOver는 차트의 레이블과 막대의 위치를 말해줍니다. 87 | 88 | ## 여러가지 기능 제공 89 | 90 | 3가지 Mark와 4가지 Marks Property를 이용하여 여러 종류의 그래프를 그릴 수 있습니다. 91 | 92 | ![스크린샷 2022-11-01 오후 6.33.21](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.33.21.png) 93 | 94 | Bar + X Position + Y Position = 막대 차트 95 | 96 | ![스크린샷 2022-11-01 오후 6.33.24](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.33.24.png) 97 | 98 | Point + X Position + Y Position = 점 차트 99 | 100 | ![스크린샷 2022-11-01 오후 6.33.39](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.33.39.png) 101 | 102 | Line + X Position + Y Position = 꺽은 선 차트 103 | 104 | ![스크린샷 2022-11-01 오후 6.33.47](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.33.47.png) 105 | 106 | Line + Point + X + Y + Foreground = 색깔로 구분되는 점과 꺽은 선을 합친 차트 107 | 108 | ![스크린샷 2022-11-01 오후 6.34.44](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.34.44.png) 109 | 110 | 이외에도 Area, Rule, Rectangle Mark를 제공하고, Symbol Size와 Line Style Property도 존재합니다. 111 | 112 | ![스크린샷 2022-11-01 오후 6.35.00](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.35.00.png) 113 | 114 | 정말 다양한 차트를 그려볼 수 있습니다. 115 | 116 | ![스크린샷 2022-11-01 오후 6.35.48](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%206.35.48.png) 117 | 118 | 다양한 접근성을 제공하고 모든 애플 플랫폼에서 사용 가능합니다. -------------------------------------------------------------------------------- /TIL/2022/WWDC21_discover_and_curate_swift_packages_using_collections.md: -------------------------------------------------------------------------------- 1 | # Discover and curate Swift Packages using collections 2 | 3 | 4 | 5 | - [New "Add Package" Workflow](#new-add-package-workflow) 6 | - [Using Collections](#using-collections) 7 | - [Creating Collection](#creating-collection) 8 | - [Signing key와 certificate 만들기](#signing-key와-certificate-만들기) 9 | 10 | 11 | 12 | ## New Add Package Workflow 13 | 14 | Xcode13부터 Swift package를 쉽게 추가하는 방법이 생겼습니다.
15 | 16 | 1. Numerics 모듈이 추가되지 않은 프로젝트 파일에 import한다면 아래와 같이 오류가 납니다. 17 | 18 | 스크린샷 2022-10-02 오후 11 38 07 19 | 20 | 2. Search 버튼을 누르면 swift-numerics 패키지를 추가하는 플로우로 이동합니다. 21 | 22 | 스크린샷 2022-10-02 오후 11 40 07 23 | 24 | 3. 이곳에서는 많은 정보를 확인할 수 있습니다. 최신 버전, 만든 이, 저작권, 리드미 등등..
또한 Release History에서 릴리즈 노트까지 확인 가능합니다. 25 | 26 | 4. Add Packages 버튼을 클릭하면 xcode가 해당 버전에 대한 선택 가능한 프로덕트를 제공합니다. 원하는 프로덕트를 선택하고 타겟에 추가합니다. 27 | 28 | 스크린샷 2022-10-02 오후 11 45 54 29 | 30 | 5. 추가된 Package는 2곳에서 확인이 가능합니다. 31 | 32 | - Project 선택 -> Swift Packages Tab에서 확인 (현재 Xcode 14.0.1에선 Package Depedency 탭입니다.) 33 | - Target 선택 -> Frameworks, Libraries, and Embedded Content phase에서 확인 34 | 35 | 36 | 37 | ## Using Collections 38 | 39 | Collections는 HTTPS 통신으로 받아오는 JSON 파일입니다. package URL 리스트, 메타데이터(서머리, 패키지 버전에 관한 자세한 정보)를 포함하고 있습니다. 아래 사진은 collection의 JSON의 일부입니다. 리드미 URL, 서머리(요약), 패키지 URL, 그리고 버전에 관한 정보들이 있는 것을 확인할 수 있습니다. 40 | 41 | 스크린샷 2022-10-02 오후 11 56 02 42 | 43 | Swift Package Manager(SwiftPM)는 이 collectino을 캐싱해놓고 맥에서 사용할 수 있도록 관리합니다. 즉, 버전 관리 툴입니다. 44 | 45 | 46 | 47 | ## Creating Collection 48 | 49 | [swift package collection generator](https://github.com/apple/swift-package-collection-generator)에 가서 클론 받고 자신의 collection을 만들 수 있습니다. 이 툴은 collection을 생성하는데 필요한 정보들을 자동으로 모아주고 항상 모든 output을 최신 버전으로 생성해줍니다. 이 툴은 패키지 URL들로 작성된 JSON 파일을 인풋으로 받습니다. collection에 서명하는 툴도 있습니다. 서명하는 것은 옵셔널이긴 하지만, 작성자를 확인할 수 있고, collection의 무결성을 보호할 수 있습니다. 50 | 51 | 아래는 collection을 만들기 위해 필요한 input JSON파일입니다. 약간의 메타 정보를 필요로 합니다. 이름, 키워드, 오버뷰, 작성자에 대한 정보. 모두 Xcode에 collection이 추가될 때 보여질 정보들입니다. 여기서 가장 중요한 것은 package URL입니다. 52 | 53 | 스크린샷 2022-10-03 오전 12 22 39 54 | 55 | 이 정보뿐만 아니라 package에 대한 추가 메타 데이터도 작성이 가능합니다. 56 | 57 | 스크린샷 2022-10-03 오전 12 29 52 58 | 59 |
60 | 61 | Collection을 생성하는 과정은 아래와 같습니다. 62 | 63 | 1. generator가 input.json 파일을 통해 collection.json 파일을 만듭니다. 64 | 2. Signing key와 Certificate를 추가하여 collection에 서명을 합니다. (옵셔널) 65 | - 만일 서명한다면 무기한 서명 collection입니다. 만료되지 않습니다. 66 | 3. 서명된 collection, 또는 서명되지 않은 collection을 배포합니다. (웹서버에 등록, 혹은 바로 추출) 67 | 68 | 스크린샷 2022-10-03 오전 12 29 02 69 | 70 | certificate, Signing key 그리고 input.json 모두 준비가 되었고, generator까지 다운로드 받았으면 해당 루트 디렉토리에서 아래 명령어를 실행합니다. 71 | 72 | ```swift 73 | // collection.json 생성하기 74 | package-collection-generate input.json collection.json --verbose --auth-token [깃헙토큰] 75 | 76 | // collection-signed.json 생성하기 (서명) 77 | package-collection-sign collection.json collection-signed.json developer-key.pem developer-cert.cer 78 | ``` 79 | 80 | 그리고 collection을 웹 서버에 올려주고 해당 URL을 이용하여 swift package-collection에 추가합니다. (참고로 저는 Github에 올렸습니다.) 81 | 82 | ```swift 83 | swift package-collection list 84 | swift package-collection add https://raw.githubusercontent.com/wody-d/spm-collections/main/collection.json 85 | swift package-collection refresh 86 | swift package-collection search --keywords wwdc21 87 | 88 | swift package-collection remove https://raw.githubusercontent.com/wody-d/spm-collections/main/collection.json 89 | ``` 90 | 91 | 스크린샷 2022-10-03 오전 1 50 31 92 | 93 | 94 | 95 | ### cf1) Signing key와 certificate 만들기 96 | 97 | 2가지를 만들기 위해서는 애플 개밝자 계정이 있어야 합니다. 먼저 키체인 접근에서 인증서 지원 -> 인증 기관에서 인증서 요청을 누르고 `CertificateSigningRequest.certSigningRequest` 파일을 생성합니다. 이때 해당 파일을 더블 클릭한 후, 이메일을 체크하고 디스크에 저장 옵션을 선택한 후 계속 버튼을 눌러 파일을 생성합니다. 98 | 99 | 생성된 파일을 [Apple Developer Certificates](https://developer.apple.com/account/resources/certificates/list)에 가서 Certificates 옆에 + 버튼을 누르고 Swift Package Collection Certificate 옵션을 선택한 후 다음 버튼을 누릅니다. 미리 생성해둔 certSigningRequest 파일을 넣고 다음 버튼을 누릅니다. Certificate가 생성되면 다운로드합니다. 생성된 Certificate를 더블 클릭한 후 로컬 키체인에 등록합니다. 키체인 접근에서 등록된 certificate을 오른쪽 클릭한 후 내보내기를 선택합니다. p12파일로 비밀번호를 입력한 후 내보내기를 누릅니다. 해당 파일을 아래와 같은 명령어를 입력하면 private key(pem키)를 얻을 수 있습니다. 100 | 101 | ```bash 102 | openssl pkcs12 -nocerts -in developer-key.p12 -out developer-key.pem && openssl rsa -in developer-key.pem -out developer-key.pem 103 | ``` 104 | 105 | 106 | 107 |
108 | 109 | - https://developer.apple.com/videos/play/wwdc2021/10197/ 110 | - https://theswiftdev.com/how-to-create-a-swift-package-collection/ 111 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:19_advanced_graphics_and_animations_for_ios.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.19 (수) 6 | 7 | 8 | 9 | ![스크린샷 2022-10-19 오전 2.31.43](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-10-19%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.31.43.png) 10 | 11 | 오늘은 Core Animation과 OpenGL ES, Graphics Hardware 프레임워크에 대해 다뤄보겠습니다. 12 | 그리고 마지막엔 UIKit의 UIBurEffect와 UIVibrancyEffect를 알아보겠습니다. 13 | 14 | ## Core Animation Pipline 15 | 16 | > 파이브라인이 행해지는 곳 17 | > Application -> Render Server -> GPU -> (Display) 18 | 19 | 20 | 21 | ![스크린샷 2022-10-19 오전 2.36.40](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-10-19%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.36.40.png) 22 | 23 | 5단계로 이루어집니다. (2021년도에 단계가 위 사진과 다르게 아래와 같이 나옵니다) 24 | 25 | > Event발생 -> Commit -> Render prepare -> Render execute -> Display 26 | 27 | 총 3개의 Frame동안 진행되거나 더 여러개의 frame동안 진행될 수 있습니다.(hitch 발생) 어쨋거나 파이프라인은 병렬적으로 진행되기 때문에 앱은 잘 작동합니다. (high refresh rate) 28 | 29 | 30 | 31 | 32 | 33 | ## Commit Transaction 34 | 35 | Commit Transaction에 대해서 자세히 살펴보겠습니다. 36 | 37 | 1. Layout - Set up the views 38 | 2. Display - Draw the views 39 | 3. Prepare - Additional Core Animation work 40 | 4. Commit - Package up layers and send them to render server 41 | 42 | > 직독직해보다 영어가 더 이해가 쉬워서 그대로 가져왔습니다. 43 | 44 | ### Layer 단계 45 | 46 | - `layoutSubviews`가 불립니다. 47 | - 이 단계에서는 view hierarchy에 `addSubview`를 이용해 layer를 추가합니다. 48 | - 콘텐츠를 채우거나 데이터베이스에서 무엇인가를 합니다. (localized strings) 49 | - Usually CPU and I/O bound 50 | 51 | ### Display 단계 52 | 53 | - `drawRect`가 불립니다. 54 | - String이 그려집니다. 55 | - 애플은 CGContext를 통해 이를 렌더링합니다. (core graphic) 56 | - Usually CPU or memory bound 57 | 58 | ### Prepare 단계 59 | 60 | - Image 디코딩 (뷰계층에 이미지가 있을시) 61 | - Image 변환 (GPU로 변환이 되지않는 이미지가 있을 경우) 62 | 63 | ### Commit 단게 64 | 65 | - Layer 정보 담고 render server한테 보냅니다. 66 | - 반복작업 (재귀) 67 | - layer 트리가 복잡할 경우 비싼 작업입니다. 68 | 69 | > Layer 트리는 가능하다면 가장 평평하게 (flat) 만드는 것이 이 commit 단계에서는 가장 효율적입니다. 70 | 71 | ## Animation 72 | 73 | > 3가지 프로세스로 이루어집니다. 74 | 75 | 1. (Application) 애니메이션을 생성하고 View hierarchy를 업데이트합니다. 76 | 77 | (ex) `UIView.animate(withDuration:animations:completion:)` ) 78 | 79 | 2. (Applcation) 애니메이션을 준비하고 commit합니다. 80 | 81 | (위 단계에서 본 commit Transaction의 `layoutSubviews`와 `drawRect`단계입니다.) 82 | 83 | 3. (Render server) 애니메이션이 commit된 순간, render server에서 해당 커밋 내용(UI 변경 사항)을 받습니다. 84 | 85 | ## Rendering concepts 86 | 87 | ### 타일 기반 렌더링 88 | 89 | - 화면은 NxN 픽셀단위의 타일로 쪼개져있습니다. 90 | - 각 타일은 SoC 캐시에 저장되어있습니다. 91 | - 지오메트리는 타일 버킷(tile bucket)으로 분할됩니다. 92 | - 모든 지오메트리는 제출된 후, Rasterization을 시작할 수 있습니다. 93 | 94 | 예를들면, 95 | 96 | - CoreAnimation의 CALayer는 2개의 삼각형입니다. 97 | - 두개의 (파란색) 삼각형을 위 이미지에서 보면 여러 타일에 걸쳐져 있습니다. 98 | - GPU는 각 타일의 (파란색) 삼각형을 분할하기 시작하여 (빨간색 삼각형 형성) 각 타일을 개별적으로 렌더링할 수 있습니다. 99 | 100 | > 빨간색이 GPU가 하는일입니다. CALayer는 큰 2개의 삼각형이라고하면 GPU는 이를 더 쪼개서 개별적으로 렌더링합니다. 101 | 102 | ### Rendering pass 103 | 104 | - render server가 commit된 뷰 계층을 디코딩했다면, OpenGL 또는 메탈을 이용해 렌더링을 시작합니다. 105 | - render server는 GPU한테 명령합니다. 106 | - GPU는 명령 buffer를 받고 작업을 시작합니다. 107 | - Tiler stage(타일 단계): 정점 처리는 정점 정점 셰이더가 실행되는 곳입니다. 이것은 모든 정점을 화면 공간으로 변환하므로 실제 타일링인 두 번째 단계를 수행할 수 있습니다. 108 | - Tiler 단계의 출력을 파라미터 버퍼라고 합니다. 109 | - GPU는 다음 중 하나가 될 때까지 대기합니다. 110 | - 모든 지오메트리가 계산되어서 파라미터 버퍼에 놓였을 때까지 111 | - 파라미터 버퍼가 꽉 찰 때까지 112 | - Renderer stage: 픽셀 연산이 끝난 시점 113 | - Renderer stage의 출력을 Renderer 버퍼라고 불립니다. 114 | 115 | ### Masking 116 | 117 | ![mask](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/mask.png) 118 | 119 | - Pass1에서, texture layer를 마스킹합니다. 120 | - Pass2에선, texture 콘텐츠 layer를 마스킹합니다. 121 | - Pass3에선, compositioning pass라고도 불리며, layer 마스크는 콘텐츠 layer와 합성되어 파란색 아이콘이 보여집니다. 122 | 123 | > texture가 정확히 무슨 의민지 이해하지 못했습니다. 다만, layer mask -> layer content -> 합성 적용 의 단계를 거쳐 렌더링이 되는 것을 알 수 있습니다. 124 | 125 | ### UIBlurEffect 126 | 127 | ![uiblur](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/uiblur.png) 128 | 129 | - Pass 1에선, 콘텐츠를 렌더링합니다. 130 | - Pass 2에선, 렌더링된 콘텐츠를 축소합니다. 이 축소는 디바이스(하드웨어)에 따라 다릅니다. 131 | - Pass 3와 4에선, Horizontal, Vertical blur를 적용합니다. 같이 실행되도 될 것 같지만 따로 실행함으로써 메모리를 많이 아낄 수 있습니다. 132 | - 마지막 Pass 5에선, blur처리가 끝난 이미지를 확대하고 tint를 입힙니다. 133 | 134 | Performance: 135 | 136 | - Pass 1, 5는 GPU 시간을 더 필요로 합니다. (content 렌더링, 확대, 틴트) 137 | - 이 둘 사이의 Pass들은 context switching을 위한 GPU idle time(유휴시간)은 짧지만, 그 사이 pass들이 많아질수록, 이 시간은 길어집니다. 이 idle 타임은 각각 0.1~0.2ms이고 이 말은 `UIBlurEffect`를 적용하는 것은 0.4~0.8ms의 GPU idle time이 소요됩니다. 한프레임이 16.67ms이기 때문에 제 시간안에 끝날 수 있습니다. 138 | - UIBlurEffect가 더 어두울수록 성능은 향상합니다. 139 | 140 | > GPU idle time은 GPU가 대기상태라는 뜻입니다. 즉 CPU의 연산작업이 길어질수록 길어지는 시간입니다. 낮은 성능의 CPU, 혹은 비효율적인 루프나 검색 시간 등에 의해서 GPU로 다시 context switching이 되기까지의 대기시간이 발생하게 됩니다. 141 | 142 | ### UIBlurEffect + UIVibrancyEffect (생동감 주는 효과) 143 | 144 | ![vibrancy](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/vibrancy.png) 145 | 146 | - 1 ~ 5 pass는 blur처리를 위해 사용됩니다. 147 | - 그리고나서, texture layer content를 렌더링합니다. 148 | - blur 결과와 texture 결과를 합성합니다. 149 | 150 | Performace: 151 | 152 | - Blur처리를 위한 Pass는 비싼 작업입니다. 153 | - Pass 6의 렌더링 작업 또한, 콘텐츠 크기에 따라서 비싼 작업입니다. 154 | - filter pass는 가장 비싼 작업이 됩니다. 155 | - 추가된 2개의 Pass들로 인해 GPU idle time이0.6~1.2ms로 더 길어집니다. 156 | 157 | Tips: 158 | 159 | - screen의 작은 일부만 vibrancy를 적용합니다. 160 | - enable rasterization(CALayer의 `shouldRasterize`), -> GPU를 이용해 이미지를 합성 가능 161 | 162 | > rasterization란? 렌더링 파이프라인의 2번째 단계입니다. 자세한 내용은 더 깊게 들어가야하기 때문에 나중에 공부합시다:) 163 | 164 | - CALayer의 `allowsGroupOpacity` 프로퍼티는 항상 disable 처리합니다. 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | - https://www.wwdcnotes.com/notes/wwdc14/419/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Woody Tip

2 | 3 | 4 | 5 | 🍾 : Blog Post 6 | 7 | 🍺 : GitHub issues & markdown 8 | 9 | # 🍎 iOS 10 | 11 | - [애플 로그인 구현하기 - Sign In with Apple](https://github.com/wody-d/woody-iOS-tip/issues/13) 12 | - [카카오톡 공유하기 기능 구현하기 ](https://github.com/wody-d/woody-iOS-tip/issues/15) 13 | - [UIKit에서 Preview 기능 사용하기 (Preview in UIKit](https://github.com/wody-d/woody-iOS-tip/issues/14) 14 | - [상수 정리를 위한 namespace의 타입을 enum을 사용하는 이유 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/12) 15 | - [Style share 코드 스타일 컨벤션 정리](https://github.com/wody-d/woody-iOS-tip/issues/11) 16 | - [Xcode 파란색 폴더와 회색 폴더의 차이](https://github.com/wody-d/woody-iOS-tip/issues/10) 17 | - [UIKit에서 SwiftUI 사용하기](https://github.com/wody-d/woody-iOS-tip/issues/8) 18 | - [iOS15의 UIButton Configuration 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/20) 19 | - [UIButton - 로딩 버튼, 토글 버튼, 팝업 버튼 구현하기](TIL/TIL_2022:10:16_button_configuration_more.md) 20 | - [커스텀 UIButton 만들기](https://github.com/wody-d/woody-iOS-tip/issues/22) 21 | - [UITableViewCell Configuration 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/25) 22 | - [WidgetKit 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/29) 23 | - [iOS에서 Composition Pattern 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/31) 24 | - [CollectionView Custom Layout 만들기](https://wodyios.tistory.com/55) 25 | - [ViewWillAppear 메소드 호출 후에 항상 ViewDidAppear 메소드가 호출될까? - 생명주기에 관해 몰랐던 점 알아보기](https://wodyios.tistory.com/74) 26 | - [Intrinsic Content Size 알아보기](https://wodyios.tistory.com/3) 27 | - [SwiftLint 적용하기](https://wodyios.tistory.com/12) 28 | - [Custom Color 관리하기](https://wodyios.tistory.com/11) 29 | - [System Color 알아보기](https://wodyios.tistory.com/10) 30 | - [Thread, DispatchQueue 알아보기](https://wodyios.tistory.com/9) 31 | - [Swift 메모리 관리 알아보기](https://wodyios.tistory.com/8) 32 | - [Coordinator 패턴 알아보기](https://wodyios.tistory.com/40) 33 | - [UICollectionView custom layout 구현하기](https://wodyios.tistory.com/55) 34 | - [Blur 처리하는 3가지 방법 알아보기](TIL/TIL_2022/10/19_blur_effect_3_ways.md) 35 | - [Swift 5.5 이후 Concureency 알아보기](TIL/TIL_2022:10:11_concurrency_swift5.5_basic.md) 36 | - [@Published 프로퍼티 래퍼를 protocol에 정의하기](https://wodyios.tistory.com/78) 37 | 38 | ##### Test 39 | 40 | - [TDD Cycle 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/40) 41 | - [TDD App Setup에 대해 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/41) 42 | - [TDD 기본 메소드들 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/42) 43 | - [XCTActivity 알아보기](TIL/TIL_2022:10:05_xcactivity.md) 44 | - [Test Doubles에 대해 알아보기](TIL/TIL_2022:10:01_test_doubles_dummy_fake_stub_mock_spy.md) 45 | 46 | ##### Animation 47 | 48 | - [UIView.transition 알아보기](https://wodyios.tistory.com/16) 49 | - [UIView.animate의 animationOptions 알아보기](https://wodyios.tistory.com/15) 50 | - [UIView.Keyframe animation 알아보기](https://wodyios.tistory.com/17) 51 | - [UIBezierpath 알아보기](TIL/TIL_2022:10:19_uibezierpath.md) 52 | - [CoreBasicAnimation 알아보기](https://wodyios.tistory.com/25) 53 | - [CAReplicatorLayer 알아보기](https://wodyios.tistory.com/69) 54 | - [Layer 마스킹에 대해 알아보기 (`layer.mask`)](https://wodyios.tistory.com/70) 55 | - [AnchorPoint와 position의 관계 알아보기 (feat. SwiftUI에선 anchorPoint가 없다.)](https://wodyios.tistory.com/71) 56 | - [Layer에 CoreAnimation 체이닝하기 (feat. 무한 체이닝은 가능할까?)](https://wodyios.tistory.com/73) 57 | - [UIBezierpath 경로따라 물체를 움직이기](https://wodyios.tistory.com/72) 58 | - [Core Animation KeyframeAnimation, CAMediaTiming 알아보기](TIL/TIL_2022:10:22_making_core_animation.md) 59 | 60 | ##### 3rd party library 61 | 62 | - [Moya 알아보기](https://wodyios.tistory.com/23) 63 | - [Observables 알아보기](https://wodyios.tistory.com/27) 64 | - [Publish, Behvior, Relay 알아보기](https://wodyios.tistory.com/44) 65 | - [Operator - Filtering](https://wodyios.tistory.com/45) 66 | - [Operator - Transforming](https://wodyios.tistory.com/46) 67 | - [Operator - Combining](https://wodyios.tistory.com/47) 68 | - [PinLayout, FlexLayout 알아보기](TIL/TIL_2022:10:13_pinlayout_flexlayout.md) 69 | - [ReactorKit Pulse 알아보기](TIL/TIL_2022:10:02_reactorkit_pulse.md) 70 | 71 | ##### HIG 72 | 73 | - [HIG - Feedback 읽어보기](TIL/TIL_2022:10:04_hig_feedback.md) 74 | - [HIG에 왜 toast가 없을까?](https://www.wodyd.com/ios-hig-toast/) 75 | 76 | ##### ETC 77 | 78 | - [모듈화의 이유 알아보기 / 정적, 동적 라이브러리 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/28) 79 | 80 | # 🍏 SwiftUI 81 | 82 | - [List에서 content offset 주는 방법 알아보기](https://github.com/wody-d/woody-iOS-tip/issues/19) 83 | - [SwiftUI에서 UIKit 사용하기](https://github.com/wody-d/woody-iOS-tip/issues/8) 84 | 85 | # 🍊 Swift 86 | 87 | - [Self vs self](https://wodyios.tistory.com/2) 88 | - [Closure](https://wodyios.tistory.com/7) 89 | - [String index](https://wodyios.tistory.com/41) 90 | - [Task - init, cancel](TIL/TIL_2022:10:11_task_init_cancel.md) 91 | - [Swift Naming](TIL/TIL_2022:10:02_swift_naming.md) 92 | 93 | ##### Design Patterns 94 | 95 | - [Open-Closed Principle](https://wodyios.tistory.com/42) 96 | - [Liskov Substitution Principle](https://wodyios.tistory.com/43) 97 | - [Interface Segregation Principle](https://wodyios.tistory.com/49) 98 | - [Dependency Inversion Principle](https://wodyios.tistory.com/52) 99 | - [Factory Pattern](https://wodyios.tistory.com/54) 100 | 101 | ##### Data structure 102 | 103 | - [Union-Find](https://wodyios.tistory.com/38) 104 | - [Heap](TIL/TIL_2022:10:10_heap.md) 105 | 106 | # ☠️ Error 107 | 108 | - [ScrollView 안에 tableView에서 tableView의 높이에 따라 scrollView의 높이를 변경하는 상황에서 변경되지 않는 이슈 해결하기 (feat. dynamic cell)](https://github.com/wody-d/woody-iOS-tip/issues/46) 🍺 109 | - [Homebrew에서 M1과 intel의 패키지 파일 경로차이로 생기는 문제점 해결하기](https://github.com/wody-d/woody-iOS-tip/issues/7) 110 | - [Tuist 빋르 시 FlexLayout, PinLayout 발생하는 오류 해결하기](https://github.com/wody-d/woody-iOS-tip/blob/main/TIL/TIL_2022:10:14_tuist_layout_flexlayout_build_error.md) 111 | 112 | # 🔀 Git 113 | 114 | - [Stash 기능 알아보기](https://wodyios.tistory.com/58) 115 | - [Pull Request & Code Review 알아보기](https://wodyios.tistory.com/14) 116 | 117 | # 🚌 ETC 118 | 119 | - [타이포라에서 이미지 바로 업로드하기](TIL/TIL_2022:10:10_upload_image_with_typora.md) 120 | -------------------------------------------------------------------------------- /Sample/ReactorKitSampleProject/ReactorKitSample/ReactorKitSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:05_xcactivity.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2020.10.05 (수) 6 | 7 |
8 | 9 | [XCTActivity가 무엇일까?](https://github.com/MashUp-iOS-Test-Master/Test-Cookie-Cook-Book/issues/7) 10 | 11 |
12 | 13 | XCTActivity를 Activity라고 합니다. 14 | 15 | 이 Activity를 이용하여 긴 테스트 메소드들을 단계단계로 작게 나눌 수 있습니다. 각 Activity는 테스트 코드 블록으로 이루어졌고 이름을 지정할 수 있습니다. 여러개끼리 중첩할 수 있기 때문에 서로를 호출할 수도 있습니다. 복잡하고, multistep 테스트들을 하나로 묶어 리팩토링하기 때문에 Xcode 테스트 리포트를 간단히 만들 수 있습니다. 16 | 17 | XCTActivity의 기능 2가지를 소개합니다. 18 | 19 | 1. **Organize Long Test Methods into Substeps
긴 테스트 메소드를 Substep으로 쪼개기** 20 | 21 | 바로 예를 들어보겠습니다. 로그인 UI 테스트는 3가지 단계로 나눌 수 있습니다. 1. Login Window 띄우기 2. 비밀번호 입력하기 3. Login Window 닫기. 아래 코드와 같이 각 단계의 activity에 이름을 지정할 수 있습니다. Activity는 XCTContext로 표시되는 현재 테스트에 대해 실행됩니다. 아래와 같이 `XCTContext`의 [runActivityNamed:block:](https://developer.apple.com/documentation/xctest/xctcontext/2887132-runactivitynamed) 클래스 메소드를 호출하고 내부 실행 block을 실행합니다. 22 | 23 | ```swift 24 | // ✅ 긴 테스트 메소드를 substep으로 쪼갬 25 | // substep은 activity로 명세 26 | func testLogin() throws { 27 | openLoginWindow() 28 | enterPassword(for: .member) 29 | closeLoginWindow() 30 | } 31 | 32 | func openLoginWindow() { 33 | XCTContext.runActivity(named: "Open login window") { activity in 34 | let loginButton = app.buttons["Login"] 35 | 36 | XCTAssertTrue(loginButton.exists, "Login button is missing.") 37 | XCTAssertTrue(loginButton.isHittable, "Login button is not hittable.") 38 | XCTAssertFalse(app.staticTexts["Logged In"].exists, "Logged In label is visible and should not be.") 39 | 40 | loginButton.tap() 41 | 42 | let loginLabel = app.staticTexts["Login:"] 43 | XCTAssertTrue(loginLabel.waitForExistence(timeout: 3.0), "Login label is missing.") 44 | } 45 | } 46 | 47 | ``` 48 | 49 | 2. **Build Utility Methods from Common Test Substeps
자주 사용되는 테스트 단계를 메소드로 만들기** 50 | 51 | 어드민 로그인, 멤버 로그인 등 로그인할 수 있는 권한이 다양하여 모든 경우를 테스트해야하는 상황이 있습니다. 매번 아이디와 비밀번호를 입력해야하는 테스트 코드를 작성해야합니다. 이 경우 아이디 비밀번호를 입력하는 단계를 Activity로 만들어 재사용할 수 있습니다. 52 | 53 | ```swift 54 | // ✅ 자주 사용되는 테스트 단계 메소드로 만들기 55 | // activity로 명세 56 | func login(for userType: TestUserType) -> Result { 57 | return XCTContext.runActivity(named: "Login") { activity in 58 | performLoginUITests(for: userType) 59 | 60 | guard app.staticTexts["Logged In"].exists else { 61 | let screenshot = app.windows.firstMatch.screenshot() 62 | let attachment = XCTAttachment(screenshot: screenshot) 63 | attachment.lifetime = .keepAlways 64 | activity.add(attachment) 65 | return .failure(TestLoginError.invalidLogin) 66 | } 67 | return .success(userType) 68 | } 69 | } 70 | 71 | func testAdminLoginFeatures() throws { 72 | let loginResult = login(for: .admin) 73 | try XCTAssertTrue(loginResult.get() == .admin) 74 | 75 | XCTAssertTrue(app.buttons["Admin Features"].exists, "Missing Admin Features button.") 76 | XCTAssertFalse(app.buttons["Member Features"].exists, "Member Features button is visible and should not be.") 77 | } 78 | 79 | func testMemberLoginFeatures() throws { 80 | let loginResult = login(for: .member) 81 | try XCTAssertTrue(loginResult.get() == .member) 82 | 83 | XCTAssertFalse(app.buttons["Admin Features"].exists, "Admin Features button is visible and should not be.") 84 | XCTAssertTrue(app.buttons["Member Features"].exists, "Missing Member Features button.") 85 | } 86 | 87 | ``` 88 | 89 | `XCTContext`는 `XCTestCase` 서브클래스 안 뿐만 아니라 테스트 타겟 어디서든 사용할 수 있습니다. `XCUIApplication` 또는 `XCUIElement` 의 서브 클래스 메소드 내에서도 가능합니다. 90 | 91 |
92 | 93 | 이 글을 읽으면 의문이 하나 듭니다. 테스트를 단계별로 나누기 위해, 또 재사용이 쉽도록 하기 위해서는 메소드로 만들기만 해도 가능합니다. 하지만 메소드로 리팩토링하면서 굳이 왜 Activity로 만드는 것일까? 94 | 95 | ### XCTActivity 96 | 97 | > A named substep of a test method. 98 | 99 | `XCTActivity` 타입은 테스트 메소드의 단계의 이름을 지정할 수 있게 해주기 때문입니다. 테스트 이름 뿐만 아니라, `XCTAttachment` 기능들을 추가로 제공합니다. 100 | 101 |
102 | 103 | ### XCTContext 104 | 105 | > A proxy for the current testing context 106 | 107 | `XCTContext`는 테스트 케이스에서 직접 테스트에 대해 XCTActivity를 실행할 수 있는 방법을 제공합니다. 이 문장만 보면 뜻이 와닿지 않습니다. 하지만 위 글 [Grouping Tests into Substeps with Activities](https://developer.apple.com/documentation/xctest/activities_and_attachments/grouping_tests_into_substeps_with_activities)를 읽어보면 activity를 만들어주기 위한 객체라는 것을 알 수 있습니다. 긴 UI 테스트 또는 integration test(이게 뭐지)를 재사용을 위해 쪼갤 수 있고 테스트 리포트를 단순화시킬 수 있습니다. [runActivity(named:block:)](https://developer.apple.com/documentation/xctest/xctcontext/2923506-runactivity)메소드 108 | 109 | 를 이용하여 실행합니다. 110 | 111 |
112 | 113 | ### runActivity(named:block:) 114 | 115 | activity를 생성하고 실행합니다. class 타입 메소드로, name과 block을 매개변수로 받습니다. name은 activity 이름으로 테스트 결과에 표시될 이름이고, block은 해당 activity에서 수행될 테스트 코드입니다. 116 | 117 | ```swift 118 | class func runActivity( 119 | named name: String, 120 | block: (XCTActivity) throws -> Result 121 | ) rethrows -> Result 122 | ``` 123 | 124 |
125 | 126 | ### XCTAttachment 127 | 128 | > Data from a test method’s execution, such as a file, image, screenshot, data blob, or ZIP file
파일, 이미지, 스크린샷, 데이터 등과 같이 테스트 메소드으로부터 나오는 데이터들 129 | 130 | [Adding Attachments to Tests, Activities, and Issues](https://developer.apple.com/documentation/xctest/activities_and_attachments/adding_attachments_to_tests_activities_and_issues)를 읽어야 합니다. 131 | 132 | `XCTAttachment`는 여러가지 initializer가 있습니다.
Creating Attachments from... 133 | 134 | - Data 135 | - Files and Folders 136 | - Images and Screenshots 137 | - Objects 138 | - Strings 139 | 140 | Attachment의 Lifetime도 설정할 수 있습니다. 141 | 142 | ```swift 143 | enum Lifetime: Int, @unchecked Sendable { 144 | case deleteOnSuccess 145 | case keepAlways 146 | } 147 | ``` 148 | 149 | Attachment의 Metadata도 설정할 수 있습니다. 150 | 151 | - name 152 | - uniformTypeIdentifier: UTI 데이터 (무엇인지 모르곘습니다.) 153 | - userInfo: 딕셔너리 형태로 여러 타입의 데이터 저장가능 154 | 155 |
156 | 157 |
158 | 159 | - https://developer.apple.com/documentation/xctest/activities_and_attachments/grouping_tests_into_substeps_with_activities 160 | - https://developer.apple.com/documentation/xctest/xctcontext 161 | - https://developer.apple.com/documentation/xctest/xctcontext/2923506-runactivity 162 | - https://developer.apple.com/documentation/xctest/xctactivity -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:19_blur_effect_3_ways.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.19 (수) 6 | 7 | 8 | 9 | 원래 블로그에 작성하려한 정보이지만 티스토리가 현재 문제가 있기 떄문에 TIL에 작성했습니다. 10 | 11 | 지금까지 UIKit의 BlurEffect를 이용해서 Blur 효과를 낼 수 있다고만 알고 있었습니다. (무지..) 그런데 줄곧 아이폰을 사용하면서 아이폰의 잠금화면은 어떻게 Blur처리를 한걸까라는 생각을 했습니다. 그리고 이제야 Core Image라는 개념에 다가갈 수 있었습니다. 12 | 13 | ### UIBlurEffect를 이용하여 Image를 Blur처리하기 14 | 15 | `UIBlurEffect`는 UIKit의 UI 요소입니다. 콘텐츠의 가장 위에 Blur 효과를 얹을 수 있습니다. 16 | 17 | ```swift 18 | let blurEffect = UIBlurEffect(style: .light) 19 | let blurEffectView = UIVisualEffectView() 20 | blurEffectView.frame = CGRect(x: 0, y: 0, width: imageView.frame.width, height: 400) 21 | blurEffectView.center = imageView.center 22 | self.imageView.addSubview(blurEffectView) 23 | blurEffectView.effect = blurEffect 24 | ``` 25 | 26 | 27 | 28 | `UIVisualEffectView`를 사용하게 되면 두가지 Layer가 더 생기게 됩니다. `UIVisualEffectBackdropView` 와 `UIVisualEffectSubView`. 레이어가 더 생긴다는 의미는 이미지를 렌더링하는데 시간이 더 걸린다는 뜻입니다. 29 | 30 | 31 | 32 | UIBlurEffect를 이용하면 UIKit레벨에서 간단하게 blur효과를 처리할 수 있습니다. (Core Image 의 더 자세한 내용은 학습하지 않아도 됩니다.) 하지만, 단점이 많습니다. 커스텀이 불가능하기 때문에 built-in 시스템 필터들을 사용할 수 밖에 없습니다. 또한, 콘텐츠에 레이어가 추가되는 작업으로 콘텐츠가 직접 바뀌지 않습니다. (퍼포먼스가 떨어집니다.) 마지막으로 GPU 연산이 되지 않고 CPU 연산만 사용하는 UIKit 프레임워크 요소입니다. 33 | 34 | ### Image에 CIFilter 사용하기 35 | 36 | CIFilter는 Core Image 프레임워크의 이미지 프로세서입니다. Core Image 프레임워크는 다수의 built-in 필터들과 커스텀 필터를 만들 수 있는 기능을 제공합니다. 이는 OpenGL/OpenGL ES를 이용하여 GPU 기반의 렌더링을 통해 높은 퍼포먼스를 보여줍니다. 37 | 38 | 39 | 40 | ```swift 41 | func blurredImage(radius: CGFloat) -> UIImage { 42 | guard let ciImage = CIImage(image: self) else { return self } 43 | let filter = CIFilter(name: "CIGaussianBlur") 44 | filter?.setValue(ciImage, forKey: kCIInputImageKey) 45 | filter?.setValue(radius, forKey: kCIInputRadiusKey) 46 | guard let output = filter?.outputImage else { return self } 47 | return UIImage(ciImage: output) 48 | } 49 | ``` 50 | 51 | 52 | 53 | > radius를 10 / 60 처리한 이미지 결과입니다. 54 | 55 | 56 | 57 | 처음에 필터링을 걸쳐 나온 이미지를 보여주었는데 밑에 공백이 생겼습니다. 그래서 이것저것 찾아보니까 같은 이슈를 겪은 분들을 찾을 수 있었습니다. 58 | 59 | - https://stackoverflow.com/questions/18315684/cifilter-is-not-working-correctly-when-app-is-in-background 60 | - https://zeddios.tistory.com/m/1144 61 | 62 | 원인을 생각해본다면 "CIFilter를 거쳐 생성된 이미지의 크기가 원본 이미지보다 크다."였습니다. 사진의 모서리에 blur처리가 되었기 때문에 기존 이미지 가장자리와 달라진 것입니다. 63 | 64 | > 사실 이 원인은 아직 이해가 가지 않습니다. 그래서 이 글을 작성하고 Core Image프레임워크를 더 깊게 파보아야겠습니다:) 65 | 66 | 위 링크를 통해 CIAffineClamp 필터 -> CIGaussianBlur 필터 chaining을 통해 해결하는 방법이 적혀있습니다. 67 | 68 | ```swift 69 | func blurredImage(with context: CIContext = .init(), radius: CGFloat) -> UIImage { 70 | guard let ciImage = CIImage(image: self), 71 | let clampFilter = CIFilter(name: "CIAffineClamp") else { 72 | return self 73 | } 74 | clampFilter.setValue(ciImage, forKey: kCIInputImageKey) 75 | let blurredImage = clampFilter.outputImage?.applyingFilter("CIGaussianBlur", parameters: [ 76 | kCIInputRadiusKey: radius 77 | ]) 78 | guard let output = blurredImage, 79 | let cgimg = context.createCGImage(output, from: ciImage.extent) else { 80 | return self 81 | } 82 | return UIImage(cgImage: cgimg) 83 | } 84 | ``` 85 | 86 | 위 방식은 이미지를 전부 blur 처리하는 함수이지만 부분 처리하고 싶다면 크롭하는 과정을 추가하고 composited를 이용하여 합성하면 될 줄 알았는데 작동하지 않습니다. 87 | 88 | > 그 원인을 찾기 위해라도 Core Image 문서를 하나하나 읽어보며 뒤져보고 있습니다. 89 | 90 | ```swift 91 | // ❌ 92 | func blurredImage(with context: CIContext = .init(), radius: CGFloat, atRect rect: CGRect) -> UIImage { 93 | guard let ciImage = CIImage(image: self) else { return self } 94 | 95 | let croppedImage = ciImage.cropped(to: rect) 96 | let blurFilter = CIFilter(name: "CIGaussianBlur") 97 | blurFilter?.setValue(croppedImage, forKey: kCIInputImageKey) 98 | blurFilter?.setValue(radius, forKey: kCIInputRadiusKey) 99 | 100 | if let ciImageWithBlurredRect = blurFilter?.outputImage?.composited(over: ciImage), 101 | let outputImage = context.createCGImage(ciImageWithBlurredRect, from: ciImageWithBlurredRect.extent) { 102 | return .init(cgImage: outputImage) 103 | } 104 | return self 105 | } 106 | ``` 107 | 108 | 109 | 110 | 위는 두번의 필터 과정을 거쳐서 나온 결과물입니다. 111 | 112 | 아래는 UIBurEffect에서 레이어를 쌓는 것과는 달리 이미지 콘텐츠 자체를 변경한 것을 확인할 수 있습니다. 113 | 114 | 115 | 116 | **장단점** 117 | Core Image로 접근하게 된다면 정말 여러가지의 Image Filter를 사용할 수있고 커스텀까지 가능합니다. 또한 GPU 기반 렌더링이므로 높은 퍼포먼스를 보입니다. 그리고 CIFliter는 이미지 콘텐츠 자체에 바꿔버려 렌더링이 적용됩니다. 하지만 Core Image 개념을 잘 알아야 다루기 쉽다는 단점이 있습니다. 또한 오류가 쉽게 나는 key-value 코딩이 필요합니다. 118 | 119 | ### Metal 사용하기 120 | 121 | Metal은 GPU한테 직접적으로 연산과 그래픽 작업을 수행하게 합니다. CPu를 전혀 안쓰기 때문에 CPU는 다른 작업을 수행할 수 있습니다. CPU보다 GPU가 그래픽 처리에 더 빠르고 효율적인 이유는 병렬적인 작업에 최적화되어있기 때문입니다. Core Image 프레임워크 또한 Metal을 이용해서 GPU한테 그래픽 작업을 시킵니다. 마찬가지로 Core ML도 Metal을 이용합니다. 122 | 123 | > Metal을 사용하기 위해선 더 자세하게 들어가야합니다. 그래서 일단 Core Image 프레임워크를 먼저 공부하고 오려고 합니다. 124 | 125 | 126 | 127 | - https://betterprogramming.pub/three-approaches-to-apply-blur-effect-in-ios-c1c941d862c3 128 | -------------------------------------------------------------------------------- /Sample/CustomButton/CustomButton/CodeBasedViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeBasedViewController.swift 3 | // CustomButton 4 | // 5 | // Created by Woody on 2022/07/07. 6 | // 7 | 8 | import UIKit 9 | 10 | class CodeBasedViewController: UIViewController { 11 | @IBOutlet weak var contentView: UIView! 12 | 13 | private lazy var button1: UIButton = { 14 | $0.translatesAutoresizingMaskIntoConstraints = false 15 | $0.addTarget(self, action: #selector(button1Pressed), for: .touchUpInside) 16 | $0.backgroundColor = .black 17 | $0.setTitle("버튼1", for: .normal) 18 | $0.tintColor = .white 19 | $0.titleLabel?.font = .boldSystemFont(ofSize: 25) 20 | $0.layer.cornerRadius = 16 21 | 22 | return $0 23 | }(UIButton(type: .system)) 24 | 25 | private lazy var button2: UIButton = { 26 | $0.translatesAutoresizingMaskIntoConstraints = false 27 | $0.addTarget(self, action: #selector(button2Pressed), for: .touchUpInside) 28 | $0.backgroundColor = .black 29 | $0.setTitle("버튼2", for: .normal) 30 | $0.tintColor = .white 31 | $0.titleLabel?.font = .boldSystemFont(ofSize: 25) 32 | $0.layer.cornerRadius = 16 33 | 34 | return $0 35 | }(UIButton(type: .custom)) 36 | 37 | private lazy var button3: UIButton = { 38 | $0.translatesAutoresizingMaskIntoConstraints = false 39 | $0.addTarget(self, action: #selector(button3Pressed), for: .touchUpInside) 40 | $0.backgroundColor = .black 41 | $0.setTitle("버튼3", for: .normal) 42 | $0.tintColor = .white 43 | $0.titleLabel?.font = .boldSystemFont(ofSize: 25) 44 | $0.layer.cornerRadius = 16 45 | $0.setImage(UIImage(systemName: "paperplane"), for: .normal) 46 | 47 | return $0 48 | }(UIButton(type: .system)) 49 | 50 | private lazy var button4: UIButton = { 51 | $0.translatesAutoresizingMaskIntoConstraints = false 52 | $0.addTarget(self, action: #selector(button4Pressed), for: .touchUpInside) 53 | $0.configuration?.baseBackgroundColor = .systemBlue 54 | $0.configuration?.cornerStyle = .dynamic 55 | $0.configuration?.background.cornerRadius = 16 56 | $0.configuration?.title = "버튼4" 57 | $0.configuration?.image = UIImage(systemName: "paperplane") 58 | $0.configuration?.imagePlacement = .trailing 59 | 60 | var container = AttributeContainer() 61 | container.font = UIFont.boldSystemFont(ofSize: 25) 62 | 63 | $0.configuration?.attributedTitle = AttributedString("버튼4", attributes: container) 64 | 65 | return $0 66 | }(UIButton(configuration: .filled())) 67 | 68 | private lazy var button5: UIButton = { 69 | $0.translatesAutoresizingMaskIntoConstraints = false 70 | $0.addTarget(self, action: #selector(button4Pressed), for: .touchUpInside) 71 | $0.configuration?.baseBackgroundColor = .systemBlue 72 | $0.configuration?.cornerStyle = .dynamic 73 | $0.configuration?.background.cornerRadius = 16 74 | $0.configuration?.title = "버튼5" 75 | $0.configuration?.image = UIImage(systemName: "paperplane") 76 | $0.configuration?.imagePlacement = .trailing 77 | 78 | var container = AttributeContainer() 79 | container.font = UIFont.boldSystemFont(ofSize: 25) 80 | 81 | $0.configuration?.attributedTitle = AttributedString("버튼5", attributes: container) 82 | 83 | return $0 84 | }(UIButton(configuration: .filled(), primaryAction: UIAction(handler: { action in 85 | print("button5 눌림") 86 | }))) 87 | 88 | override func viewDidLoad() { 89 | super.viewDidLoad() 90 | layout() 91 | } 92 | 93 | private func layout() { 94 | 95 | view.addSubview(button1) 96 | 97 | NSLayoutConstraint.activate([ 98 | button1.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32), 99 | button1.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -32), 100 | button1.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 30), 101 | button1.heightAnchor.constraint(equalToConstant: 50) 102 | ]) 103 | 104 | view.addSubview(button2) 105 | 106 | NSLayoutConstraint.activate([ 107 | button2.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32), 108 | button2.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -32), 109 | button2.topAnchor.constraint(equalTo: button1.bottomAnchor, constant: 30), 110 | button2.heightAnchor.constraint(equalToConstant: 50) 111 | ]) 112 | 113 | view.addSubview(button3) 114 | 115 | NSLayoutConstraint.activate([ 116 | button3.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32), 117 | button3.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -32), 118 | button3.topAnchor.constraint(equalTo: button2.bottomAnchor, constant: 30), 119 | button3.heightAnchor.constraint(equalToConstant: 50) 120 | ]) 121 | 122 | view.addSubview(button4) 123 | 124 | NSLayoutConstraint.activate([ 125 | button4.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32), 126 | button4.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -32), 127 | button4.topAnchor.constraint(equalTo: button3.bottomAnchor, constant: 30), 128 | button4.heightAnchor.constraint(equalToConstant: 50) 129 | ]) 130 | 131 | view.addSubview(button5) 132 | 133 | NSLayoutConstraint.activate([ 134 | button5.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32), 135 | button5.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -32), 136 | button5.topAnchor.constraint(equalTo: button4.bottomAnchor, constant: 30), 137 | button5.heightAnchor.constraint(equalToConstant: 50) 138 | ]) 139 | 140 | } 141 | 142 | @objc private func button1Pressed() { 143 | print("button1 눌림") 144 | } 145 | 146 | @objc private func button2Pressed() { 147 | print("button2 눌림") 148 | } 149 | 150 | @objc private func button3Pressed() { 151 | print("button3 눌림") 152 | } 153 | 154 | @objc private func button4Pressed() { 155 | print("button4 눌림") 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:02_reactorkit_pulse.md: -------------------------------------------------------------------------------- 1 | ### Today I Learend 2 | 3 | ---- 4 | 5 | 2022.10.02 (일) 6 | 7 |
8 | 9 | ### @Pulse (in ReactorKit) 10 | 11 | [@Pulse](https://github.com/ReactorKit/ReactorKit#pulse)라는 프로퍼티 래퍼에 대해 정리해볼게요. 12 | 13 | > `Pulse` has diff only when mutated 14 | 15 | Pulse는 변화가 있을 때에만 변화를 준다고 합니다. 사실 이 정의만 보았을 때는 잘 이해가 되지 않았어요. 그래서 직접 만들어보면서 이해해보았습니다. 아래는 버튼 2개로 이루어진 PulseViewController와 State 3개를 가지고 있는 PulseReactor입니다. 16 | 17 | ```swift 18 | class PusleViewController: UIViewController { 19 | 20 | // UI 코드 생략 21 | 22 | func bind(reactor: Reactor) { 23 | stateButton.rx.tap.map { .didTapStateButton } 24 | .bind(to: reactor.action) 25 | .disposed(by: disposeBag) 26 | 27 | pulseButton.rx.tap.map { .didTapPulseButton } 28 | .bind(to: reactor.action) 29 | .disposed(by: disposeBag) 30 | 31 | reactor.state 32 | .map { "\($0.value)" } 33 | .subscribe(onNext: { value in 34 | print("👋🏻👋🏻👋🏻 상태 1") 35 | }).disposed(by: disposeBag) 36 | 37 | reactor.state 38 | .map { $0.message } 39 | .subscribe(onNext: { message in 40 | print("🚀🚀🚀 상태 2") 41 | }).disposed(by: disposeBag) 42 | 43 | reactor.pulse(\.$error) 44 | .observe(on: MainScheduler.instance) 45 | .subscribe(onNext: { _ in 46 | print("✨✨✨ 펄스 ") 47 | }).disposed(by: disposeBag) 48 | } 49 | 50 | } 51 | ``` 52 | 53 | ```swift 54 | class PulseReactor: Reactor { 55 | 56 | enum Action { 57 | case didTapStateButton 58 | case didTapPulseButton 59 | } 60 | 61 | struct State { 62 | var message: String = "" 63 | var value: String = "" 64 | 65 | @Pulse var error: String? 66 | } 67 | 68 | var initialState: State = State() 69 | 70 | func reduce(state: State, mutation: Action) -> State { 71 | var newState = state 72 | switch mutation { 73 | case .didTapStateButton: 74 | newState.message = "메세지" 75 | case .didTapPulseButton: 76 | newState.error = "펄스" 77 | } 78 | return newState 79 | } 80 | } 81 | ``` 82 |
83 | 84 | **왜 필요할까?** 85 | 86 | Pulse는 값에 할당이 될때에 무조건 방출하는 프로퍼티 래퍼입니다. 보통 State는 값에 변화가 있든 없든 매번 방출이 됩니다. 그렇기에 값에 변화가 없어도 구독한 객체는 항상 변화가 일어납니다. 87 | 88 | ```swift 89 | reactor.state 90 | .map { "\($0.value)" } 91 | .subscribe(onNext: { value in 92 | print("👋🏻👋🏻👋🏻 상태 1") 93 | }).disposed(by: disposeBag) 94 | 95 | ``` 96 | 97 | 해당 state가 변화가 일어나지 않아도 action -> mutate를 거쳐 reduce 통해 state가 반환되기 때문입니다. 아래 코드에서 didTapStateButton 액션이 실행되어 message 상태만 변화해도 모든 상태가 반환되어 바인딩된 객체에 전달됩니다. 98 | 99 | ```swift 100 | func reduce(state: State, mutation: Action) -> State { 101 | var newState = state 102 | switch mutation { 103 | case .didTapStateButton: 104 | newState.message = "메세지" 105 | case .didTapPulseButton: 106 | newState.error = "펄스" 107 | } 108 | return newState 109 | }ㅓ 110 | ``` 111 | 112 | 이럴 때 보통 map을 통해 특정 state로 변환한 후 `distinctUntilChanged()`메소드를 통해 해당 값이 변할 때만 이벤트를 받도록 합니다. 전달되는 Element가 Equtable 프로토콜을 채택하고 있다면 이전에 전달된 값과 비교하여 값이 같다면 두번째 element부터 전달하지 않고, 값이 다르다면 element를 전달됩니다. 113 | 114 | ```swift 115 | reactor.state 116 | .map { "\($0.value)" } 117 | .distinctUntilChanged() 118 | .subscribe(onNext: { value in 119 | print("👋🏻👋🏻👋🏻 상태 1") // distinctUntilChanged 사용하 때 이슈 있음 (마지막) 120 | }).disposed(by: disposeBag) 121 | ``` 122 | 123 | 왼쪽은, value에 대해 distinctUntilChanged 없이 출력. 오른쪽은 valueㅇ 대해 distinctUntilChanged 있이 출력. 124 | 125 | 126 | 127 |
128 | 정리하면, State가 변화하든 안하든, 항상 이벤트는 전달되는데 `distinctUntilChanged()`를 통해 특정 state가 변화할 때만 이벤트를 받도록 할 수 있습니다. 129 | 130 | 그럼, 특정 state의 값이 변화하지 않을 때에도 해당 state에 값이 할당되면 이벤트를 받도록 하고 싶은 경우에는 어떻게 해야할까요? 131 | 132 | `distinctUntilChanged()`를 사용하지 않으면 되는 거 아닌가?? 라고 처음엔 생각했지만 전혀 상관 없는 state가 변화해도 이벤트가 전달되는 경우가 있기 때문에 적절한 경우가 아니예요. 이 상황에 `@Pulse`가 필요합니다! 133 | 134 |
135 | 136 | **Pulse 뜯어보기:** 137 | 138 | Pulse가 구현된 코드는 비교적 이해하기 쉬워요. value에 값이 할당되면 `riseValueUpdatedCount()`메소드가 불립니다. 그리고 valueUpdatedCount 를 1증가시켜요. 139 | 140 | ```swift 141 | public var value: Value { 142 | didSet { 143 | self.riseValueUpdatedCount() 144 | } 145 | } 146 | public internal(set) var valueUpdatedCount = UInt.min 147 | private mutating func riseValueUpdatedCount() { 148 | if self.valueUpdatedCount == UInt.max { 149 | self.valueUpdatedCount = UInt.min 150 | } else { 151 | self.valueUpdatedCount += 1 152 | } 153 | } 154 | ``` 155 | 156 | pulse() 메소드를 살펴보면, 새로운 pulse와 기존 pulse의 valueUpdatedCount를 비교하는 `distinctUntilChanged`로 구현되어 있습니다. value가 아닌 valueUpdatedCount를 비교하므로 같은 값이 할당되어도 이벤트를 방출합니다. 만일 다른 프로퍼티가 변하더라도 valueUpdatedCount는 같기 때문에 이벤트를 방출하지 않네요. 157 | 158 | ```swift 159 | 160 | extension Reactor { 161 | public func pulse(_ transformToPulse: @escaping (State) throws -> Pulse) -> Observable { 162 | return self.state.map(transformToPulse).distinctUntilChanged(\.valueUpdatedCount).map(\.value) 163 | } 164 | } 165 | 166 | ``` 167 | 168 |
169 | 170 | **Pulse로 선언하기**: 171 | 172 | Pulse 프로퍼티 래퍼로 선언하게 된다면, 해당 state가 변화가 있든 없든 할당이 된다면 이벤트가 항상 전달됩니다. 다른 state와는 전혀 상관이 없습니다. 사용법은 아래 코드와 같아요. 173 | 174 | ```swift 175 | reactor.pulse(\.$error) 176 | .observe(on: MainScheduler.instance) 177 | .subscribe(onNext: { _ in 178 | print("✨✨✨ 펄스 ") 179 | }).disposed(by: disposeBag) 180 | ``` 181 | 182 | error 프로퍼티에 할당이 될 때 이벤트가 전달됩니다. 183 | 184 | ```swift 185 | func reduce(state: State, mutation: Action) -> State { 186 | var newState = state 187 | switch mutation { 188 | case .didTapStateButton: 189 | newState.message = "메세지" 190 | case .didTapPulseButton: 191 | newState.error = "펄스" // ✅ 할당 192 | } 193 | return newState 194 | } 195 | ``` 196 | 197 |
**마무리 :** 198 | 199 | Pulse는 에러 처리를 위한 토스트 메시지나 알림창에 자주 쓰일 것 같아요. ReactorKit 레포에도 알림 메시지에 대한 예제가 있어요. 값이 변해서 UI에 변화가 생겨야 하는 경우에 쓰는 것이 아니고 항상 값은 일정한데 할당이 되는 경우에 써야하니까요. (오류메시지는 대부분 일정하니까), 또한, 화면전환을 상태로 관리할 때에도 유용할 것 같습니다. 200 | 201 | 202 | 203 | 추가로, distinctUntilChange에서 만난 이슈 하나를 공유하자면.. 아래와 같이 작성하면 swift가 타입추론을 하지 못해 오류가 나요. 204 | 205 | 스크린샷 2022-10-02 오후 2 41 42 206 | 207 | 208 | distintUntilChanged에서 비교하는 Element는 Equtable 프로토콜을 채택하는데, 이는 방출하는 Element로 비교해서..? 일듯한데.. 정확하진 않고... 조금 더 알아봐야겠어요. 209 | 210 | ```swift 211 | extension ObservableType where Element: Equatable { 212 | 213 | /** 214 | Returns an observable sequence that contains only distinct contiguous elements according to equality operator. 215 | 216 | - seealso: [distinct operator on reactivex.io](http://reactivex.io/documentation/operators/distinct.html) 217 | 218 | - returns: An observable sequence only containing the distinct contiguous elements, based on equality operator, from the source sequence. 219 | */ 220 | public func distinctUntilChanged() 221 | -> Observable { 222 | self.distinctUntilChanged({ $0 }, comparer: { ($0 == $1) }) 223 | } 224 | } 225 | ``` 226 | 227 |


228 | 229 | - https://github.com/ReactorKit/ReactorKit#pulse 230 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:11_task_init_cancel.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.11 (화) 6 | 7 |
8 | 9 |
10 | 11 | ### Task 12 | 13 | Task 인스턴스를 생성할 때, 실행할 작업이 포함될 클로저가 제공됩니다. Task는 생성 즉시 실행되며 개발자가 직접 실행하거나 스케쥴링하지 않아도 됩니다. Task 인스턴스를 생성한 후엔, 해당 Task 인스턴스와 상호작용할 수 있습니다. Task에 대한 레퍼런스만 들고 있다면 ~~Task는 실행되지만~~Task에 대한 상호작용이 가능하지만, 만약 레퍼런스를 버린다면, 상호작용이 불가능합니다. (Task의 결과를 받고 취소할 수 없습니다.) 14 | 15 | Swift는 현재(current) Task에서 분리된 작업이나 하위 작업을 지원하기 위해 `yield()`클래스 메소드를 지원합니다. 이 메소드는 비동기식이기 때문에 항상 기존 Task에서 호출됩니다. 기존 Task의 일부로 실행되는 코드에서만 해당 Task와 상호작용할 수 있습니다. 해당 Task에서 기존 Task와 상호작용하려면 Task의 static method를 호출해야합니다. 16 | 17 | > 이 말은 이해가 되지 않았지만, 현재 이해한 바로는 Task A와 A에서 파생된 B,C Task가 있다고 한다면, B와 C는 무조건 Task A 내부에서 사용해야 하고, B, C에서 Task A와 상호작용하고 싶다면 static method를 호출해야한다고 이해했습니다. 18 | 19 | Task의 실행은 Task가 실행되는 진행도라고 볼 수 있습니다. 이 진행도는 Task가 중단되거나 완료될 때 종료됩니다. 이 진행도는 `PartialAsyncTask`의 인스턴스로 표시됩니다. 사용자 지정 실행자를 구현하지 않는 한, `PartialAsyncTask`와는 상호작용할 수 없습니다. 20 | 21 | > 여기까지는 개념 설명이고, Task가 언제 사용되는지, 이 시점이 가장 궁금했습니다. 22 | > Task를 개발자가 명시적으로 사용하는 곳은 동시성을 지원하지 않는 환경에서 비동기함수를 부르고 싶을 때 사용합니다. 23 | > 즉, Task는 concurrency 환경을 제공해줍니다. 24 | 25 | ### Task Cancellation 26 | 27 | Task는 Cancellation을 나타내는 공유 매커니즘은 있지만 Cancellation을 처리하는 방법은 공유하지 않습니다. 즉, Task마다 작업을 중지하는 방법은 다릅니다. 마찬가지로, Task가 필요한 부분에 Cancellation 여부를 확인해야합니다. 긴 작업의 경우 여러 지점에서 Cancellation을 확인하고 각 지점마다 다르게 처리할 수 있습니다. 취소 여부를 확인하는 방법엔 2가지가 있습니다. 28 | 29 | 첫 번째로, `Task.checkCancellation()` 함수를 호출하여 취소 여부를 확인할 수 있습니다. 취소에 대한 다른 응답으로는, 지금까지 완료한 작업을 반환하거나 빈 결과를 반환 혹은 0 반환이 있습니다. 30 | 31 | 두 번째로, `Cancellation`입니다. 이 프로퍼티는 Bool 타입으로 취소 사유와 같은 추가 정보를 포함할 수 없습니다. 32 | 33 | ### Task 초기화하기 34 | 35 | 1. `init(priority: TaskPriority?, operation: () async -> Success)` 36 | 37 | 우선수위는 옵셔널이므로 수행할 작업을 클로저형태로 받습니다. 38 | 39 | ```swift 40 | let readTask = Task { 41 | return "Star Link" 42 | } 43 | 44 | print(await readTask.value) 45 | // Star Link 46 | ``` 47 | 48 | 2. `init(priority: TaskPriority?, operation: () async throws-> Success)` 49 | 50 | 수행할 작업이 에러를 던질 수도 있습니다. 51 | 52 | ```swift 53 | enum ReadError: Error { 54 | case cantread 55 | } 56 | let readTask = Task { 57 | 58 | throw ReadError.cantread 59 | } 60 | 61 | do { 62 | print(try await readTask.value) 63 | } catch { 64 | print("Read task failed with error: \(error)") 65 | } 66 | // Basic task failed with error: cantread 67 | ``` 68 | 69 | > 여기서 중요한 점은, Task는 생성 즉시 실행된다는 점입니다. 아래와 같이 작성해도 출력이 됩니다. 다시 말해, Task는 따로 실행시킬 필요가 없습니다. 70 | > 71 | > ```swift 72 | > let readTask = Task { 73 | > print("readTask") 74 | > return "Star Link" 75 | > } 76 | > // readTask 77 | > ``` 78 | 79 | ### Task 사용하기 80 | 81 | 위에서도 설명하긴 했지만 동시성을 지원하지 않는 환경에서 비동기함수를 부르고 싶을 때 사용합니다. 아래 예시에서는 `executeTask()`비동기 함수는 `onAppear`메소드 내에서 호출되어야 하는 상황이지만, `onAppear`는 동시성을 지원하지 않는 modifier라 바로 호출하게 되면 컴파일 오류가 납니다. 이 때 `Task`를 통해 동시성 환경을 제공합니다. 82 | 83 | ```swift 84 | var body: some View { 85 | Text("Hello, world!") 86 | .padding() 87 | .onAppear { 88 | Task { 89 | await executeTask() 90 | } 91 | } 92 | } 93 | 94 | func executeTask() async { 95 | let basicTask = Task { 96 | return "This is the result of the task" 97 | } 98 | print(await basicTask.value) 99 | } 100 | The task creates a concurre 101 | ``` 102 | 103 | ⛔️ 하지만 이곳에서 중요한 점이 있습니다. Task는 레퍼런스를 들고 있어야 한다고 방금 문서에서 읽었습니다. 레퍼런스를 들고 있지 않는다면, Task의 결과와 Task의 취소 작업을 할 수 없다고 했습니다. 그런데 위에서는 Task 인스턴스에 대한 레퍼런스를 가지고 있지 않습니다. Combine의 경우도 생각해보면, Publisher 구독은 값이 방출되기 전까지 강한 레퍼런스 참조를 유지해야합니다. 그런데 Task는 대체 무슨 일? 104 | 105 | Task는 레퍼런스를 가지고 있든 가지고 있지 않든 상관없이 무조건 작동합니다. 레퍼런스를 가지고 있는 이유는 오직 결과를 기다리고 작업을 취소하는 기회를 가지기 위해서입니다. 106 | 107 | ### Task Cancellation 해보기 108 | 109 | 아래 예제는 splash에서 랜덤이미지를 가져옵니다. 110 | 111 | ```swift 112 | struct ContentView: View { 113 | @State var image: UIImage? 114 | 115 | var body: some View { 116 | VStack { 117 | if let image = image { 118 | Image(uiImage: image) 119 | } else { 120 | Text("Loading...") 121 | } 122 | }.onAppear { 123 | Task { 124 | do { 125 | image = try await fetchImage() 126 | } catch { 127 | print("Image loading failed: \(error)") 128 | } 129 | } 130 | } 131 | } 132 | 133 | func fetchImage() async throws -> UIImage? { 134 | let imageTask = Task { () -> UIImage? in 135 | let imageURL = URL(string: "https://source.unsplash.com/random")! 136 | print("Starting network request...") 137 | let (imageData, _) = try await URLSession.shared.data(from: imageURL) 138 | return UIImage(data: imageData) 139 | } 140 | return try await imageTask.value 141 | } 142 | } 143 | ``` 144 | 145 | 랜덤 이미지를 불러오기 요청을 보내는 Task를 생성하자마자 바로 취소해보겠습니다. 146 | 147 | ```swift 148 | func fetchImage() async throws -> UIImage? { 149 | let imageTask = Task { () -> UIImage? in 150 | let imageURL = URL(string: "https://source.unsplash.com/random")! 151 | print("Starting network request...") 152 | let (imageData, _) = try await URLSession.shared.data(from: imageURL) 153 | return UIImage(data: imageData) 154 | } 155 | imageTask.cancel() // ✅ 156 | return try await imageTask.value 157 | } 158 | 159 | // Starting network request... 160 | // Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled" 161 | 162 | ``` 163 | 164 | Task를 취소했지만 위와 같이 프린트가 찍힙니다. 그 이유는 Task가 생성 즉시 실행되고, 실행되는 도중, 취소되어버려 이미지를 불러오지 못한 상황입니다. 165 | 166 | > 만약 정말 빠르게 API가 동작한다면 imageData를 불러와서 UI에 랜덤이미지가 렌더링됐을 것입니다. 167 | 168 | 위의 취소 예제에서는 어떤 시점에서 취소체크가 일어나는 지 정확하지 않습니다. (print문을 통해 어느시점인지 대략적으로는 알 수 있습니다.) 169 | 하지만 취소를 체크할 수 있는 static method를 통해 취소되는 시점을 지정할 수 있습니다. 첫 번째 메소드는 `Task.checkCancellation()`입니다. 170 | 171 | ```swift 172 | let imageTask = Task { () -> UIImage? in 173 | let imageURL = URL(string: "https://source.unsplash.com/random")! 174 | 175 | try Task.checkCancellation() // ✅ 176 | 177 | print("Starting network request...") 178 | let (imageData, _) = try await URLSession.shared.data(from: imageURL) 179 | return UIImage(data: imageData) 180 | } 181 | imageTask.cancel() 182 | 183 | // Image loading failed: CancellationError() 184 | ``` 185 | 186 | 다른 방법은, `Task.isCancalled` 프로퍼티를 통해 확인하는 것입니다. 187 | 188 | ```swift 189 | let imageTask = Task { () -> UIImage? in 190 | let imageURL = URL(string: "https://source.unsplash.com/random")! 191 | 192 | guard Task.isCancelled == false else { 193 | // Perform clean up 194 | print("Image request was cancelled") 195 | return nil 196 | } 197 | 198 | print("Starting network request...") 199 | let (imageData, _) = try await URLSession.shared.data(from: imageURL) 200 | return UIImage(data: imageData) 201 | } 202 | // Cancel the image request right away: 203 | imageTask.cancel() 204 | 205 | ``` 206 | 207 | 사실 여기까지 보았을 때, 왜 취소체크 메소드와 프로퍼티를 통해 취소 여부를 확인하는지 이해가지 않습니다. Task의 다음 코드가 실행되기 전에 취소여부를 확인하는 절차가 없어도 그 시점에 취소가 되면 자동으로 Task가 취소가 되는 것이 아닌가? 이해가 되지는 않지만 사용해보면서 이해가 되면 다시 돌아와야겠습니다. 208 | 209 |
210 | 211 | 212 | 213 | ### Ref 214 | 215 | - https://developer.apple.com/documentation/swift/task 216 | - https://www.avanderlee.com/concurrency/tasks/ 217 | 218 |
219 | 220 | 221 | 222 | 223 | ### 질문 224 | 225 | - Task 취소 체크가 왜 필요한가? 226 | 아래 2가지를 비교해보았을 때, 왜 다르게 동작하는 지 잘 이해가 가지 않는다. 실행되는 스레드의 속도에 따라서 매번 다른 결과가 나오지 않을까? (실제로 돌려보았을 때는 항상 아래 print문의 결과가 나옴) 위나 아래나 cancel되는 시점에 이미 Task는 수행되고 있지 않나? 왜 저렇게 다르게 나오지.. 227 | 228 | ```swift 229 | let imageTask = Task { () -> UIImage? in 230 | let imageURL = URL(string: "https://source.unsplash.com/random")! 231 | print("네트워크 요청 시작..") 232 | let (imageData, _) = try await URLSession.shared.data(from: imageURL) 233 | print("이미지 데이터 받아옴..") 234 | return UIImage(data: imageData) 235 | } 236 | imageTask.cancel() 237 | 238 | // print문 239 | // "네트워크 요청 시작.. 240 | ``` 241 | 242 | ```swift 243 | let imageTask = Task { () -> UIImage? in 244 | let imageURL = URL(string: "https://source.unsplash.com/random")! 245 | 246 | try Task.checkCancellation() // ✅ 247 | 248 | print("Starting network request...") 249 | let (imageData, _) = try await URLSession.shared.data(from: imageURL) 250 | return UIImage(data: imageData) 251 | } 252 | imageTask.cancel() 253 | 254 | // print문 255 | // X 256 | ``` 257 | 258 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:19_uibezierpath.md: -------------------------------------------------------------------------------- 1 | ### Today I Leanred 2 | 3 | ---- 4 | 5 | 2022.10.19 (수) 6 | 7 | 8 | 9 | ### **UIBezierPath** 10 | 11 | > A path that consists of straight and curved line segments that you can render in your custom views. 12 | 13 | view에서 렌더링할 수 있는 직선과 곡선으로 구성된 경로입니다. 이 클래스는 view의 형태를 직접 그려주기 위해서 사용합니다. 곡선, 아치형, 직선형 등으로 모양을 정의해준 후, 현재 그려지는 context에 렌더링하기 위해 추가적인 메소드를 사용할 수 있습니다. 14 | 15 | UIBezierPath 객체는 렌더링될 때 경로를 설명하는 속성들과 지오메트리의 경로들을 합칩니다. 즉, 지오메트리 및 속성을 개별적으로 설정하고 서로 독립적으로 변경할 수 있습니다. 객체를 원하는 방식으로 구성한 후에는 현재 context에서 객체를 그리도록 지정할 수 있습니다. 생성, 구성 및 렌더링 프로세스는 모두 별개의 단계로 Bezier 경로 객체를 코드에서 쉽게 재사용할 수 있습니다. 동일한 객체를 사용하여 동일한 도형을 여러번 렌더링할 수도 있으며, 연속 그리기 호출간에 렌더링옵션을 변경할 수 있습니다. 16 | 17 | 경로의 현재 지점을 조작하여 경로의 지오메트리를 설정합니다. 빈 경로 객체를 새로 만들면 현재 점이 점의되지 않으므로 명시적으로 설정해야합니다. 세그먼트를 그리지 않고 현재 점을 이동하려면 move(to:) 함수를 사용합니다. 다른 모든 함수는 경로에 선 또는 커브 세그먼트를 추가합니다. 새로운 세크먼트를 추가하는 방법은 항상 현재 지점에서 시작하여 지정한 새 지점에서 끝나는 것으로 가정합니다. 세그먼트를 추가하면 세그먼트의 끝점이 자동으로 현재 포인트가 됩니다. 18 | 19 | 하나의 Bezier 경로 객체는 열려있거나 닫힌 하위 경로가 얼마든지 포함될 수 있습니다. 각 하위 경로는 연결된 일련의 경로 세그먼트를 나타냅니다. close() 함수를 호출하면 현재 포인트에서 하위 경로의 첫 번째 포인트까지 직선 세그먼트가 추가되어 하위 경로가 닫힙니다. move(to:) 함수를 호출하면 현재 하위 경로가 종료되고(닫지 않고) 다음 하위 경로의 시작점이 설정됩니다. Bezier 경로 객체의 하위 경로는 동일한 도면 특성을 공유하므로 그룹으로 조작해야합니다. 서로 다른 특성을 가진 하위 경로를 그리려면 각 하위 경로를 자체 UIBezierPath객체에 넣어야합니다. 20 | 21 | Bezier 경로의 지오메트리 및 속성을 구성한 후에는 stoke() 와 fill() 함수를 사용하여 현재 그래픽 context에서 경로를 그립니다. stoke() 함수는 현재 stroke 색상과 Bezier경로 객체의 속성을 사용하여 경로의 윤곽선을 그립니다. 마찬가지로 fill() 함수는 현재 색을 사용하여 경로로 둘러싸인 영역을 채웁니다. 22 | 23 | Bezier 경로 객체를 사용하여 도형을 그릴뿐만 아니라 새 자르기 영역을 정의할 수 있습니다. addClip() 함수는 경로 객체로 표현된 도형을 그래픽 컨텍스트의 현재 clipping 영역과 교차합니다. 새로운 교차로 영역 내에 있는 콘텐츠만 그래픽에 렌더링됩니다. 24 | 25 | ### 사용해보기 26 | 27 | ```swift 28 | class ViewController: UIViewController { 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | let bview: BezierView = .init(frame: view.frame) 33 | bview.backgroundColor = .clear 34 | view.addSubview(bview) 35 | } 36 | } 37 | 38 | class BezierView: UIView { 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | override func draw(_ rect: CGRect) {} 48 | } 49 | ``` 50 | 51 | BezierPath를 초기화하는 법은 여러가지가 있습니다. 52 | 53 | **사각형** 54 | 55 | ```swift 56 | override func draw(_ rect: CGRect) { 57 | let path = UIBezierPath(rect: CGRect(x: bounds.midX-50, y:bounds.midY-50, width: 100, height: 100)) 58 | UIColor.systemRed.setFill() 59 | UIColor.systemYellow.setStroke() 60 | path.lineWidth = 10 61 | path.stroke() 62 | path.fill() 63 | } 64 | ``` 65 | 66 |

67 | 68 | **타원형** 69 | 70 | ```swift 71 | override func draw(_ rect: CGRect) { 72 | let path = UIBezierPath(ovalIn: CGRect(x: bounds.midX-100, y:bounds.midY-100, width: 200, height: 300)) 73 | UIColor.blue.setFill() 74 | UIColor.red.setStroke() 75 | path.lineWidth = 10 76 | path.stroke() 77 | path.fill() 78 | } 79 | ``` 80 | 81 |

82 | 83 | **둥근 사각형** 84 | 85 | ```swift 86 | override func draw(_ rect: CGRect) { 87 | let path = UIBezierPath(roundedRect: CGRect(x: bounds.midX-100, y:bounds.midY-100, width: 100, height: 200), cornerRadius: 20) 88 | UIColor.blue.setFill() 89 | UIColor.red.setStroke() 90 | path.lineWidth = 10 91 | path.stroke() 92 | path.fill() 93 | } 94 | ``` 95 | 96 |

97 | 98 | **아치형** 99 | 100 | ```swift 101 | override func draw(_ rect: CGRect) { 102 | let path = UIBezierPath(roundedRect: CGRect(x: bounds.midX-50, y:bounds.midY-50, width: 100, height: 100), byRoundingCorners: [.topLeft,.topRight], cornerRadii: .init(width: 50, height: 50)) 103 | UIColor.blue.setFill() 104 | UIColor.red.setStroke() 105 | path.lineWidth = 10 106 | path.stroke() 107 | path.fill() 108 | } 109 | ``` 110 | 111 |

112 | 113 | **곡선** 114 | 115 | ```swift 116 | override func draw(_ rect: CGRect) { 117 | let path = UIBezierPath(arcCenter: center, radius: 50, startAngle: 0, endAngle: .pi, clockwise: false) 118 | UIColor.blue.setFill() 119 | UIColor.red.setStroke() 120 | path.lineWidth = 10 121 | path.stroke() 122 | path.fill() 123 | } 124 | ``` 125 | 126 |

127 | 128 | **Initializer 정리** 129 | 130 | - [`init(rect: CGRect)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624359-init) 131 | - [`init(ovalIn: CGRect)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624379-init) 132 | - [`init(roundedRect: CGRect, cornerRadius: CGFloat)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624356-init) 133 | - [`init(roundedRect: CGRect, byRoundingCorners: UIRectCorner, cornerRadii: CGSize)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624368-init) 134 | - [`init(arcCenter: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624358-init) 135 | - clockwise: 그려질 방향 선택 (false: 위, true: 아래) 136 | 137 | ### 경로 만들기 138 | 139 | 위와 같이 Initializer을 통해 모양을 만들 수 있지만 직접 원하는 모양을 그릴 수 있습니다. 140 | 141 | ```swift 142 | override func draw(_ rect: CGRect) { 143 | let path = UIBezierPath() 144 | path.move(to: center) 145 | path.addCurve(to: .init(x: center.x + 100, y: center.y - 100), 146 | controlPoint1: .init(x: center.x + 1, y: center.y - 97), 147 | controlPoint2: .init(x: center.x + 82, y: center.y - 7)) 148 | path.addLine(to: .init(x: center.x + 100, y: center.y)) 149 | path.addLine(to: .init(x: center.x, y: center.y)) 150 | UIColor.blue.setFill() 151 | UIColor.red.setStroke() 152 | path.lineWidth = 10 153 | path.stroke() 154 | path.fill() 155 | path.close() // 모양이 완성되면 끊기 156 | } 157 | ``` 158 | 159 |

160 | 161 | - [`func move(to: CGPoint)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624343-move) 162 | - 시작점을 정할 수 있습니다. 163 | - 현재 point를 지정해줍니다. 164 | - [`func addLine(to: CGPoint)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624354-addline) 165 | - [`func addArc(withCenter: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624367-addarc) 166 | - [`func addCurve(to: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624357-addcurve) 167 | - 3차 베지어 곡선 그리기 168 | - [`func addQuadCurve(to: CGPoint, controlPoint: CGPoint)`](https://developer.apple.com/documentation/uikit/uibezierpath/1624351-addquadcurve) 169 | - 2차 베지어 곡선 그리기 170 | 171 | > 곡선을 그릴 때 참고할 만한 사이트 172 | > https://cubic-bezier.com/#.17,.67,.83,.67 173 | 174 | ### 여러 프로퍼티들 175 | 176 | - `path.stroke()` 를 이용해서 가장자리(테두리)를 그릴 수 있습니다. 177 | - `path.lineWidth = 10`을 이용해서 선의 두께를 정할 수 있습니다. 178 | - `setStroke()`를 이용해서 원하는 선 색깔을 정할 수 있습니다. 179 | - `path.lineCapStyle = .round`를 이용하면 선의 끝점이 둥글어 집니다. (시작과 끝에만 적용) 180 | - `path.lineJoinStyle = .round`를 이용하면 모든 모서리가 둥글어 집니다. 181 | - `path.fill()`을 통해 색을 채워줍니다. 182 | 183 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:01_test_doubles_dummy_fake_stub_mock_spy.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.01 (토) 6 | 7 |
8 |
9 | 개발을 하다보면 종종 mock 데이터, dummy 데이터에 대한 얘기가 많이 나와요. 저는 이 두 용어가 같은 의미라고 생각했기 때문에 임의의 데이터가 필요한 경우 "mock 데이터 만들어서 테스트해주세요." "dummy 데이터 만들어주세요"라며 사용하곤 했습니다. 점점 TDD와 아키텍처에 관심을 갖다 보니 stub과 mock에 대한 자료도 보이며 제가 기존에 알고 있던 용어들에 관해서도 급격히 혼란이 왔습니다. 🥲 그래서 오늘 드디어 공부해봤습니다. 10 | 11 | 12 | 13 | ### Test Doubles (테스트 대역) 14 | 15 | > Test Double is a generic term for any case where you replace a production object for testing purposes. — Martin Fowler 16 | 17 | 유닛 테스트를 작성할 때, production과 동일하게 작동하지만 조금 더 단순화된 버전의 객체가 필요합니다. 이런 종류의 객체들이 Test Double입니다.
그럼 이게 왜 필요할까요? 👋🏻
한 가지 예시를 들어볼게요. 18 | 19 | ```swift 20 | 21 | protocol LogoutUseCase { 22 | func logout() -> Observable 23 | } 24 | 25 | final class LogoutUseCaseImpl: LogoutUseCase { 26 | private let authRepository: AuthRepository 27 | private let accessTokenRepository: AccessTokenRepository 28 | private let userAccountRepository: UserAccountRepository 29 | init( 30 | authRepository: AuthRepository, 31 | accessTokenRepository: AccessTokenRepository, 32 | userAccountRepository: UserAccountRepository 33 | ) { 34 | self.authRepository = authRepository 35 | self.accessTokenRepository = accessTokenRepository 36 | self.userAccountRepository = userAccountRepository 37 | } 38 | 39 | func logout() -> Observable { 40 | authRepository.logout() 41 | .do { [weak self] _ in 42 | _ = self?.accessTokenRepository.deleteAccessToken() 43 | _ = self?.userAccountRepository.deleteLocalUserAccount() 44 | } 45 | } 46 | } 47 | 48 | ``` 49 | 50 | 위 코드는 로그아웃 기능을 구현한 예시입니다.
LogoutUseCaseImpl 객체를 테스트하기 위해선 3가지 프로퍼티가 필요해요. (조금 단순화시킴)
`AuthRepository`, `AccessTokenRepository`, `UserAccountRepository`
logout 메소드에 대해 테스트 할 때 오류가 난다면, 어디서 난 오류인지 바로 캐치할 수 있을까여? 로그아웃 네트워크 실패일 수도 있고, 토큰을 저장하는 로컬 디비에서 삭제할 때 실패할 수도 있어요. 51 | 52 | 즉, 객체를 테스트하기 위해서는 해당 객체가 완전히 독립적이어야 합니다. (의존성 0%)
53 | 54 | 하지만 객체들끼리 의존할 수 밖에 없기 때문에 해당 메소드를 테스트하기 위해 의존하는 객체들에 대한 대역을 만들어야 합니다. 위 예시에선 `AuthRepository`와 `AccessTokenRepository`, `UserAccountRepository` 3가지 객체들에 대한 대역을 이용해 logout 메소드를 테스트해볼 수 있습니다. 55 | 56 |
블로그에 나온 다른 예시를 하나 더 들어볼게요. 57 | 58 | ViewModel이 Repository 프로퍼티를 가지고 있다고 해봅니다.
이때 ViewModel 테스트 코드를 작성할 때, 실제 네트워크 통신하는 Repository가 필요할까요? 59 |
60 | 61 | 아닙니다. UseCase를 테스트하는데 실제 Repository가 있다면 어디서 오류가 어디서 날 지 알 수 없습니다. 또한 해당 UseCase에서 2개 이상의 Repository를 사용한다면 더욱 혼란이 올거예요. Repository는 Repository Test를 별도로 작성해서 네트워크 통신이 잘 되는지 테스트를 작성하면 됩니다. ViewModel에서는 ViewModel이 가진 로직만 테스트하면 됩니다. 62 | 63 | 즉, 현재 테스트 대상에 대해서만 집중해야 합니다. 64 | 65 | 위의 예시처럼 test double은 실제 객체를 대신해서 동작하는 대역이지만, 테스트 목적에 따라 테스트 대역들이 여러가지가 있고 쓰임이 제각각 다릅니다. 66 | 67 | - Dummy 68 | - Fake 69 | - Stub 70 | - Mock 71 | - Spy 72 | 73 | ### Dummy 74 | 75 | **정의** 76 | 77 | Dummy 객체는 가장 기본적인 테스트 대역입니다. `placeholder`와 같이 아무런 기능을 수행하지 않는 인스턴스화 된 객체입니다. (단지 자리만 채워주는 역할) sut가 의존하고 있기 때문에 인스턴스화된 객체는 필요하지만, 테스트 할 때 사용하지 않아요. 78 | 79 | 저는 이때까지 tableview나 collectionview를 테스트할 때 생성하는 객체들을 더미라고 불렀어요. 😓 하지만, 그것들은 데이터로써 쓰임이 있기 때문에 dummy가 아니라는 사실을 알게 되었네요. 80 | 81 | **Unit Test에서 사용하기** 82 | 83 | 객체를 초기화해줄 때 사용됩니다. 아래 코드는 `DatabaseReader`와 `EmailServiceHelper`의 더미 서비스의 구현입니다. 84 | 85 | ```swift 86 | // Dummy DatabaseReader 87 | class DummyDatabaseReader: DatabaseReader { 88 | func getAllStock() -> Result<[Television], Error> { 89 | return .success([]) 90 | } 91 | } 92 | 93 | // Dummy EmailServiceHelper 94 | class DummyEmailServiceHelper: EmailServiceHelper { 95 | func sendEmail(to address: String) {} 96 | } 97 | 98 | ``` 99 | 100 | 이 더미들은 `TelevisionWarehouse`의 초기화를 테스트하는데 사용합니다. 더미들은 아무런 기능을 수행하지 않아요. 101 | 102 | ```swift 103 | func testWarehouseInitSuccess() { 104 | // given 105 | let dummyReader = DummyDatabaseReader() 106 | let dummyEmailService = DummyEmailServiceHelper() 107 | // when 108 | let warehouse = TelevisionWarehouse(dummyReader, emailServiceHelper: dummyEmailService) 109 | // then 110 | XCTAssertNotNil(warehouse) 111 | } 112 | ``` 113 | 114 | ### Fake 115 | 116 | **정의** 117 | 118 | Fake는 실제 객체와 핵심 로직은 동일하게 작성 되어 있지만, 다른 불필요 요소는 제거 되어 있는 객체입니다. (불필요 요소: 시간이 걸리거나 복잡한 configuration 등등) 119 | 120 | 즉, 실제 객체보다 훨씬 간단한 버전으로 작성된 객체입니다. 121 | 122 | sut가 의존하는 객체의 핵심 로직이 제대로 수행되어야 테스트가 가능한 경우에 사용합니다. 실제 객체만큼 정교하진 않지만 동작의 구현 일부만 담당하게 돼요. 예를 들어, 데이터베이스와 연결하거나 네트워크 요청을 보낼 때가 있습니다. 로그를 남기거나 실제로 외부 의존성에 의존해야 하는 부분들을 제거할 수 있어요. 123 | 124 | **Unit Test에서 사용하기** 125 | 126 | (이해가 쉬워서 [이 블로그](https://jiseobkim.github.io/swift/2022/02/06/Swift-Test-Double(부제-Mock-&-Stub-&-SPY-이런게-뭐지-).html)에서 가져온 예시입니다!) 127 | 128 | 계산기 메소드 중 sum이라는 것은 a+b값을 리턴하는 로직을 테스트해야 합니다. 단순히 a와 b의 합을 리턴하는지를 확인하면 되는데 실제 코드에선 sum에 대한 로그를 서버에 남기기 위해 통신 하는 코드가 들어 있거나, 결과를 다른 객체에 전달하는 역할을 하는 코드도 들어가 있을 수 있습니다. 129 | 130 | ```swift 131 | protocol Calculator { 132 | func sum(x: Int, y: Int) -> Int 133 | } 134 | class CalculatorImp: Calculator { 135 | let b: Some 136 | let c: Some2 137 | 138 | init(b: Some, c: Some2) { 139 | self.b = b 140 | self.c = c 141 | } 142 | 143 | func sum(x: Int, y: Int) -> Int { 144 | // 불필요 145 | b.callMethod() 146 | b.callMethod2() 147 | b.callMethod3() 148 | 149 | // 불필요 150 | if x > 0 { 151 | c.callLogMethod() 152 | } 153 | 154 | // 핵심 코드 155 | let result = x + y 156 | return result 157 | } 158 | } 159 | 160 | ``` 161 | 162 | 주석에 쓰여진 작업들은 핵심로직이 아니므로 불필요합니다.
이런 경우 아래와 같이 간소화할 수 있어요. 163 | 164 | ```swift 165 | class CalculatorFake: Calculator { 166 | func sum(x: Int, y: Int) -> Int { 167 | let result = x + y 168 | return return 169 | } 170 | } 171 | ``` 172 | 173 | ### Stub 174 | 175 | **정의** 176 | 177 | Stub은 미리 정해진 데이터들을 항상 반환하는 기능을 가진 가짜 객체입니다. 실제 환경에서 테스트하기 어려운 것들을 테스트하고 싶을 때 굉장히 용이하다고 하네요. 이것도 네트워크 연결 오류나 서버 오류 같은 상황에 적합하다고 합니다. (이렇게 말하는 이유가 있다.. Fake랑 다른점을 모르겠지만 일단 ㅇㅋ...) 178 | 179 | **Unit Test에서 사용하기** 180 | 181 | 데이터베이스를 읽는 과정에서 오류가 난 상황을 테스트해본다고 하면, 아래와 같이 정해진 오류를 리턴하는 기능을 가진 Stub 객체를 만들 수 있어요. 182 | 183 | ```swift 184 | class StubDatabaseReader: DatabaseReader { 185 | 186 | enum StubDatabaseReaderError: Error { 187 | case someError 188 | } 189 | 190 | func getAllStock() -> Result<[Television], Error> { 191 | return .failure(StubDatabaseReaderError.someError) 192 | } 193 | } 194 | ``` 195 | 196 | 197 | 198 | ### Stub vs Fake 199 | 200 |
201 | 202 | Fake와 Stub이 언뜻 비슷해보입니다. 만일 Repository를 프로퍼티로 가지는 UseCase를 테스트하는 상황이라고 하면, 이 Repository는 Fake 객체로도, Stub 객체로도 만들 수 있어요. 여기서, 차이점은 Stub은 미리 지정된 객체만 리턴이 된다는 것이고, Fake는 요청을 받고 응답을 리턴한다라고 할 수 있습니다. Repository를 데이터베이와 연결된 객체라고 한다면, Fake 객체는 데이터베이스를 배열로 대체하여 실제 기능처럼 구현하고 Stub 객체는 미리 지정한 값으로 구현합니다. 203 | 204 | ```swift 205 | class FakeRepositoryImpl: DatabaseReader { 206 | var storedArray: [String] = [] 207 | 208 | func readUsersNickname() -> [String] { 209 | return storedArray 210 | } 211 | } 212 | 213 | class StubRepositoryImpl: DatabaseReader { 214 | func readUsersNickname() -> [String] { 215 | return ["미리", "지정된", "객체"] 216 | } 217 | } 218 | ``` 219 | 220 | 221 | 222 | ### Mock 223 | 224 | **정의** 225 | 226 | 리턴값과 콜백값이 중요하지 않고 **해당 메소드가 잘, 얼마나 실행 되었는 지** 중요할 때 사용하는 객체입니다. 리턴값과 콜백이 없는 메소드는 state로 테스트하기 어렵고, 실제 환경에서 메소드가 제대로 작동되었는 지 확인하기 어려울 때 사용합니다. 예시로, 이메일 시스템이 있어요. 이메일 시스템은 테스트할 때마다 실제로 이메일을 보내면 안될 뿐더러 실제로 이메일이 제대로 갔는지 확인하기도 어렵습니다. 이런 상황엔 이메일 시스템이 제대로 호출되었는 지를 확인해야 하고 mock 객체를 사용합니다. 227 | 228 | **Unit Test에서 사용하기** 229 | 230 | (이것두 이해가 쉬워서 [이 블로그](https://jiseobkim.github.io/swift/2022/02/06/Swift-Test-Double(부제-Mock-&-Stub-&-SPY-이런게-뭐지-).html)에서 가져온 예시입니다!) 231 | 232 | 네트워크 호출 코드가 잘 작동했는지에 대해 테스트할 때도 Mock 객체를 사용합니다, 233 | 234 | ```swift 235 | class NetworkMock: Network { 236 | 237 | var networkCallCount = 0 238 | 239 | func network(handler: @escaping (Void) -> Void) { 240 | var networkCallCount += 1 241 | } 242 | } 243 | ``` 244 | 245 | `networkCallCount` 변수를 만들고 246 | 247 | ```swift 248 | class MockTests: XCTestCase { 249 | private var sut: Company! 250 | private var printerMock: PrinterMock! 251 | 252 | override func setUp() { 253 | printerMock = PrinterMock() 254 | // 1. PrintImp 대신 Mock을 넣어준다. 255 | sut = Company(printer: printerMock) 256 | } 257 | 258 | func test_call_network() { 259 | // given 260 | // when 261 | sut.submit() 262 | 263 | // then 264 | // 2. printerMock에 메소드가 잘 호출되었는지 테스트 한다. 265 | XCTAssertEqual(printerMock.networkCallCount, 1) 266 | } 267 | } 268 | ``` 269 | 270 | 해당 메소드가 잘 호출되었는 지 확인합니다. 271 | 272 | ### Spy 273 | 274 | **정의** 275 | 276 | Mock + Stub, 상태도 행위도 테스트하는 객체라고 합니다! 미리 지정한 결과를 리턴해주고 해당 메소드가 제대로 불렸는지 확인하는 `count` 변수를 만들어서 카운팅해줍니다. 그런데 굳이 이 객체를 써야할 상황이 올까라는 의문이 듭니다.. 리턴이 온다는 것은 해당 메소드가 제대로 불렸다는 것을 입증하는 것인데.? 🤔 277 | 278 | ---- 279 | 280 | ### 마무리 281 | 282 | 다른 개발자와 의사소통시 사소하지만 조금이라도 명확한 의사소통을 할 수 있게 된 것 같네용! test double을 상황에 맞게 사용하는 훈련하기! 283 | 284 | 285 | 286 | - https://medium.com/mobil-dev/unit-testing-and-test-doubles-in-swift-5b5e93e68512 287 | - https://jiseobkim.github.io/swift/2022/02/06/Swift-Test-Double(부제-Mock-&-Stub-&-SPY-이런게-뭐지-).html 288 | - https://tecoble.techcourse.co.kr/post/2020-09-19-what-is-test-double/ 289 | - https://velog.io/@leeyoungwoozz/Test-Doubles 290 | - https://swiftsenpai.com/testing/test-doubles-in-swift/ 291 | - https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da 292 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:10_heap.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.10 (월) 6 | 7 |
8 | 9 | **힙이란?** 10 | 11 | 힙 트리는 여러 개의 값 중에서 가장 크거나 작은 값을 빠르게 찾기 위해 만든 이진 트리로, 짧게 힙이라고 줄여서 부르기도 합니다. 힙은 완전 이진 트리의 형태를 띠어야 하고, 부모의 값은 항상 자식들의 값보다 크거나 작아야합니다. 따라서 루트 노드는 자식 노드보다 항상 크거나 작기 때문에 최댓값, 최솟값은 항상 O(1) 안에 찿을 수 있습니다. 12 | 13 | **왜 항상 완전 이진트리여야 하나요?** 14 | 15 | 단순히 최댓값, 최솟값을 O(1)안에 찾기 위해, "항상 완전 이진 트리 형태"일 필요는 없습니다. 완전 이진 트리를 사용하는 이유는 삽입하거나 삭제할 때의 속도떄문입니다. 16 | 17 | **시간 복잡도** 18 | 19 | 데이터의 삽입과 삭제의 시간복잡도는 모두 **O(logN)**입니다. 20 | 21 | ### Swift로 구현해보기 22 | 23 | 완전 이진트리의 자료구조는 배열이 됩니다. 따라서 배열의 index를 트리로 나타내는 방법을 알아야 합니다. 부모노드의 번호가 1이라고 할 때, 왼쪽 자식노드와 오른쪽 자식 노드의 Index는 2와 3으로 아래의 식을 가집니다. 24 | 25 | 왼쪽 자식 노드 index = 부모노드 index * 2
오른쪽 자식 노드 index = 부모노드 index * 2 + 1
부모 노드 index = (왼쪽) 오른쪽 자식 노드 index / 2 26 | 27 | > 다른 식으로도 표현할 수 있습니다. (index가 0부터 시작일 때) 28 | > 29 | > 왼쪽 자식 노드 index = 부모노드 index * 2 + 1
오른쪽 자식 노드 index = 부모노드 index * 2 + 2
부모 노드 index = ((왼쪽) 오른쪽 자식 노드 index - 1) / 2 30 | 31 | 그럼 먼저 Heap 구조체를 만들겠습니다. 32 | 33 | ```swift 34 | struct Heap { 35 | var tree: [T] = [] 36 | 37 | init(withData data: T) { 38 | tree.append(data) 39 | tree.append(data) 40 | } 41 | } 42 | ``` 43 | 44 | `Comparable` 프로토콜을 준수하는 이유는 부모와 자식노드의 값을 비교하기 때문입니다. 또한, 초기화할 때 data를 두번 삽입하는 이유는 배열의 0번 index는 사용하지 않기 때문에 아무 의미없는 값을 채워넣기 위함입니다. 45 | 46 | > 0부터 시작할 경우, 한 번만 삽입합니다. 47 | 48 |
49 | 50 | **데이터의 삽입** 51 | 52 | 맥스 힙의 경우만 생각해 볼 때, 아래의 과정을 따릅니다. 53 | 54 | 1. 가장 끝의 자리에 노드를 삽입한다. 55 | 2. 그 노드와 부모 노드를 서로 비교한다. 56 | 3. 노드가 부모 노드보다 크다면 서로 값을 교환한다. 57 | 4. 부모 노드가 가장 클 때까지 2~3번 과정을 반복한다. 58 | 59 | ```swift 60 | mutating func insert(_ data: T) { 61 | // 비었다면 초기화가 필요합니다. 62 | if tree.isEmpty { 63 | tree.append(data) 64 | tree.append(data) 65 | return 66 | } 67 | 68 | // 가장 뒤에 노드를 삽입합니다 69 | tree.append(data) 70 | 71 | var newNodeIndex = tree.count - 1 72 | var parentNodeIndex = newNodeIndex / 2 73 | while tree[parentNodeIndex] < tree[newNodeIndex] { 74 | tree.swapAt(parentNodeIndex, newNodeIndex) 75 | newNodeIndex = parentNodeIndex 76 | parentNodeIndex = newNodeIndex / 2 77 | 78 | // 현재 노드가 루트노드이면 더이상 비교하지 않습니다. 79 | if newNodeIndex == 1 { break } 80 | } 81 | } 82 | ``` 83 | 84 | tree안에 아무 값도 존재하지 않을 때 insert가 된다면 초기화할 때처럼 0번 인덱스에 쓸모없는 값을 채워줘야 합니다. 값이 있다면, 마지막에 값을 삽입하고 2~3번의 과정을 반복합니다. 트리의 루트 노드의 index는 1이므로 쓸모없는 값이 들어있는 index인 0으로 가지 않도록 탈출 조건을 넣어줘야 합니다. 85 | 86 |
87 | 88 | **데이터의 삭제** 89 | 90 | 데이터의 삭제는 최댓값인 루트노드를 제거합니다. 91 | 92 | 1. 루토 노드를 제거한다. 93 | 2. 루트 노드 자리에 가장 마지막 노드를 삽입한다. 94 | 3. 루트자리에 올라간 마지막 노드와 자식 노드를 비교한다. 95 | 4. 아래 조건을 따른다. 96 | 1. 부모보다 더 큰 자식이 없다면 교환하지 않고 끝낸다. 97 | 2. 부모보다 더 큰 자식이 하나만 있으면 그 자식하고 교환한다. 98 | 3. 부모보다 더 큰 자식이 둘이라면 자식들 중 큰 값과 교환한다. 99 | 5. 해당 노드가 더이상 교환되지 않을 때까지 3~4번 과정을 반복한다. 100 | 101 | ```swift 102 | func leftChildIndex(ofParentAt index: Int) -> Int { 103 | return (2 * index) 104 | } 105 | 106 | func rightChildIndex(ofParentAt index: Int) -> Int { 107 | return (2 * index) + 1 108 | } 109 | 110 | mutating func pop() -> T? { 111 | guard !tree.isEmpty else { return nil } 112 | 113 | if tree.count == 2 { 114 | let value = tree[1] 115 | tree.removeAll() 116 | return value 117 | } 118 | 119 | tree.swapAt(1, tree.count - 1) 120 | let value = tree.removeLast() 121 | 122 | moveDown(from: 1) 123 | 124 | return value 125 | } 126 | 127 | mutating func moveDown(from index: Int) { 128 | var parent = index 129 | while true { 130 | let left = leftChildIndex(ofParentAt: parent) 131 | let right = rightChildIndex(ofParentAt: parent) 132 | var popped = parent 133 | 134 | if left <= count && tree[left] > tree[popped] { 135 | popped = left 136 | } 137 | if right <= count && tree[right] > tree[popped] { 138 | popped = right 139 | } 140 | if popped == parent { 141 | return 142 | } 143 | tree.swapAt(parent, popped) 144 | parent = popped 145 | } 146 | } 147 | ``` 148 | 149 | tree의 index가 1부터 시작하기 때문에 앞에 조건 2가지가 붙습니다. 비었을 때 nil 반환, 2개일 때 모든 삭제하고 마지막값 반환하기. 150 | 151 | 테스트해보자. 152 | 153 | ```swift 154 | var heap = Heap(withData: 10) 155 | heap.insert(50) 156 | heap.insert(20) 157 | heap.insert(100) 158 | print(heap.tree) 159 | 160 | heap.pop() 161 | heap.pop() 162 | print(heap.tree) 163 | 164 | // [10, 100, 50, 20, 10] 165 | // [10, 20, 10] 166 | ``` 167 | 168 | 잘 나옵니다! 169 | 170 | 최대힙트리를 구현했지만 최소힙트리를 구현하려면 마이너스로 값을 변환하여 집어넣을 수도 있고 구현한 코드의 부등호를 반대로 바꾸면 됩니다. 반대로 바꿀려면 많이 바꾸어야 하기 때문에 initializer에서 받을 수도 있습니다. 171 | 172 | ```swift 173 | struct Heap { 174 | var tree: [T] = [] 175 | let sort: (T, T) -> Bool 176 | init(withData data: T, sort: @escaping (T, T) -> Bool) { 177 | tree.append(data) 178 | tree.append(data) 179 | self.sort = sort 180 | } 181 | 182 | // ... 183 | } 184 | 185 | var heap = Heap(withData: 40, sort: >) 186 | ``` 187 | 188 | > 지금까지 구현한 코드 189 | 190 | ```swift 191 | struct Heap { 192 | var tree: [T] = [] 193 | 194 | init(withData data: T) { 195 | tree.append(data) 196 | tree.append(data) 197 | } 198 | 199 | var isEmpty: Bool { 200 | return tree.isEmpty 201 | } 202 | 203 | var count: Int { 204 | return tree.isEmpty ? 0 : tree.count - 1 205 | } 206 | 207 | func top() -> T? { 208 | return tree.isEmpty ? nil : tree[1] 209 | } 210 | 211 | mutating func insert(_ data: T) { 212 | // 비었다면 초기화가 필요합니다. 213 | if tree.isEmpty { 214 | tree.append(data) 215 | tree.append(data) 216 | return 217 | } 218 | 219 | // 가장 뒤에 노드를 삽입합니다. 220 | tree.append(data) 221 | 222 | var newNodeIndex = tree.count - 1 223 | var parentNodeIndex = newNodeIndex / 2 224 | while tree[parentNodeIndex] < tree[newNodeIndex] { 225 | tree.swapAt(parentNodeIndex, newNodeIndex) 226 | newNodeIndex = parentNodeIndex 227 | parentNodeIndex = newNodeIndex / 2 228 | 229 | // 현재 노드가 루트노드이면 더이상 비교하지 않습니다. 230 | if newNodeIndex == 1 { break } 231 | } 232 | } 233 | 234 | func leftChildIndex(ofParentAt index: Int) -> Int { 235 | return (2 * index) 236 | } 237 | 238 | func rightChildIndex(ofParentAt index: Int) -> Int { 239 | return (2 * index) + 1 240 | } 241 | 242 | mutating func pop() -> T? { 243 | guard !tree.isEmpty else { return nil } 244 | 245 | if tree.count == 2 { 246 | let value = tree[1] 247 | tree.removeAll() 248 | return value 249 | } 250 | 251 | tree.swapAt(1, tree.count - 1) 252 | let value = tree.removeLast() 253 | moveDown(from: 1) 254 | 255 | return value 256 | } 257 | 258 | mutating func moveDown(from index: Int) { 259 | var parent = index 260 | while true { 261 | let left = leftChildIndex(ofParentAt: parent) 262 | let right = rightChildIndex(ofParentAt: parent) 263 | var popped = parent 264 | 265 | if left <= count && tree[left] > tree[popped] { 266 | popped = left 267 | } 268 | if right <= count && tree[right] > tree[popped] { 269 | popped = right 270 | } 271 | if popped == parent { 272 | return 273 | } 274 | tree.swapAt(parent, popped) 275 | parent = popped 276 | } 277 | } 278 | } 279 | ``` 280 | 281 | > 인덱스가 0부터 시작할 때의 Heap의 구현 282 | 283 | ```swift 284 | struct Heap1 { 285 | var tree = [T]() 286 | let sort: (T, T) -> Bool 287 | 288 | init(sort: @escaping (T, T) -> Bool) { 289 | self.sort = sort 290 | } 291 | 292 | var isEmpty: Bool { 293 | return tree.isEmpty 294 | } 295 | 296 | var count: Int { 297 | return tree.count 298 | } 299 | 300 | func peek() -> T? { 301 | return tree.first 302 | } 303 | 304 | func leftChildIndex(ofParentAt index: Int) -> Int { 305 | return (2 * index) + 1 306 | } 307 | 308 | func rightChildIndex(ofParentAt index: Int) -> Int { 309 | return (2 * index) + 2 310 | } 311 | 312 | func parentIndex(ofChildAt index: Int) -> Int { 313 | return (index - 1) / 2 314 | } 315 | 316 | mutating func insert(_ element: T) { 317 | tree.append(element) 318 | moveUp(from: tree.count - 1) 319 | } 320 | 321 | mutating func moveUp(from index: Int) { 322 | var child = index 323 | var parent = parentIndex(ofChildAt: child) 324 | while child > 0 && sort(tree[child], tree[parent]) { 325 | tree.swapAt(child, parent) 326 | child = parent 327 | parent = parentIndex(ofChildAt: child) 328 | } 329 | } 330 | 331 | mutating func pop() -> T? { 332 | guard !tree.isEmpty else { return nil } 333 | 334 | if tree.count == 2 { 335 | let value = tree[1] 336 | tree.removeAll() 337 | return value 338 | } 339 | 340 | tree.swapAt(1, tree.count - 1) 341 | let value = tree.removeLast() 342 | moveDown(from: 1) 343 | return value 344 | } 345 | 346 | mutating func moveDown(from index: Int) { 347 | var parent = index 348 | while true { 349 | let left = leftChildIndex(ofParentAt: parent) 350 | let right = rightChildIndex(ofParentAt: parent) 351 | var candidate = parent 352 | 353 | if left < count && sort(tree[left], tree[candidate]) { 354 | candidate = left 355 | } 356 | if right < count && sort(tree[right], tree[candidate]) { 357 | candidate = right 358 | } 359 | if candidate == parent { 360 | return 361 | } 362 | tree.swapAt(parent, candidate) 363 | parent = candidate 364 | } 365 | } 366 | } 367 | ``` 368 | 369 | -------------------------------------------------------------------------------- /TIL/2022/WWDC21_protect_mutable_state_with_swift_actors.md: -------------------------------------------------------------------------------- 1 | ### Protect mutable state with Swift actors 2 | 3 | ---- 4 | 5 | 2022.10.12 (수) 6 | 7 | 동시성 프로프래밍을 할 때 주의해야하는 기본적인 문제 중 하나는 **data race**입니다. data race는 두 개이상의 쓰레드가 하나의 공유 자원에 접근할 때, 하나 이상의 작업이 공유자원을 바꾸는 write작업일 때 일어납니다. 8 | 9 | 하나의 예제를 봅시다. 10 | 11 | ```swift 12 | class Counter { 13 | var value = 0 14 | 15 | func increment() -> Int { 16 | value = value + 1 17 | return value 18 | } 19 | } 20 | ``` 21 | 22 | 숫자를 카운팅하는 Counter 클래스가 있습니다. 23 | 24 | ```swift 25 | struct ContentView: View { 26 | 27 | var body: some View { 28 | Button("Data Race 체크", role: .destructive) { 29 | let counter = Counter() 30 | 31 | Task.detached { 32 | print("첫 번째:", counter.increment()) // data race 33 | } 34 | 35 | Task.detached { 36 | print("두 번째:", counter.increment()) // data race 37 | } 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | 버튼이 눌릴 때, 카운터의 숫자를 증가시킵니다. 이 작업은 `Task.detached`로 인해 해당 작업들은 병렬적으로 실행되어 Data race가 발생할 수 있는 상황입니다. print 결과로 (1,2) (2,1) (2,2) (1,1)... 다양하게 나올 수 있습니다. 44 | 45 | > 직접 해봤지만 (1,2)의 결과만 나옵니다. 더 복잡한 형태로 구성하여야 Data Race가 일어나는 것을 확인할 수 있을 것 같습니다. 46 | 47 | Data race를 방지하는 방법은 value 타입을 사용하는 것입니다. value-type을 `let` 으로 선언하는 것이야말로 정말 변하지 않는 state를 만들기 때문에 동시에 실행되는 여러 task들의 접근에서부터 안전할 수 있습니다. 48 | 49 | 그럼 struct으로 선언해보겠습니다. 그리고 `increment`메소드는 mutating 키워드를 붙이고, Counter는 struct 타입이므로 `var` 선언하겠습니다. 50 | 51 | ```swift 52 | struct Counter { 53 | var value = 0 54 | 55 | mutating func increment() -> Int { 56 | value = value + 1 57 | return value 58 | } 59 | } 60 | 61 | var counter = Counter() 62 | 63 | Task.detached { 64 | print(counter.increment()) // data race 65 | } 66 | 67 | Task.detached { 68 | print(counter.increment()) // data race 69 | } 70 | ``` 71 | 72 | 이 또한 Data Race가 발생합니다. 왜냐하면 각각의 Task에서 counter가 캡처되기 때문입니다. 아예 빌드가 되지 않고 컴파일 에러가 납니다. 73 | 74 | 75 | 76 | 77 | 78 | 비동기적인 작업들이 하나의 공유 자원을 공유하고 있을 경우, 동기화가 필요해보입니다. 기존에는 Atomics, Locks, Serial dispatch queue와 같은 도구들을 사용했습니다. ~~(Atomics와 Locks는 처음 들어봅니다.)~~ 이 도구들을 사용할 때 신중을 가하지 않으면 data race는 쉽게 일어나고 맙니다. 그래서 Actor가 등장했습니다. 79 | 80 | ![zzal](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/zzal.jpeg) 81 | 82 | ### Actor의 등장 83 | 84 | Actor는 공유 자원의 동기화 작업을 도와줍니다. 자신만의 state를 가지고 있기 때문에 프로그램과는 고립되어 있습니다. state에 접근하는 유일한 방법은 actor를 통해 접근하는 것입니다. 기존 data race 해결 방식으론 *개발자로 인한 생길 수 있는 오류*가 많았다면 actor는 컴파일 에러를 통해 예방합니다. 85 | 86 | > Actor는 Swift의 새로운 타입입니다. 87 | > class와 struct과 같이 actor는 프로퍼티, 메소드, initializer, subcript 등과 같은 일반적인 기능들을 제공합니다. 또한 protocol을 채택할 수 있고, extension을 통해 확장 가능합니다. 그리고 자원을 공유하기 위한 용도이기 때문에 Class와 마찬가지로 레퍼런스 타입입니다. 오직 다른 점 하나는, actor의 인스턴스 데이터들은 프로그램으로부터 고립시키기 때문에 동기화가 보장됩니다. 88 | 89 | 바로 사용해보도록 합시다. 90 | 91 | ```swift 92 | actor Counter { 93 | var value = 0 94 | 95 | func increment() -> Int { 96 | value = value + 1 97 | return value 98 | } 99 | } 100 | struct ContentView: View { 101 | var body: some View { 102 | Button("Data Race 체크", role: .destructive) { 103 | let counter = Counter() 104 | Task.detached { // Task 1 105 | print(await counter.increment()) 106 | } 107 | 108 | Task.detached { // Task 2 109 | print(await counter.increment()) 110 | } 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | class와 마찬가지로 프로퍼티와 메소드가 선언되었습니다. 하지만 다른 점은, value 값에 동시에 접근이 불가합니다. 이 뜻은 actor 내의 코드가 동작되고 있다면, increment 메소드는 그 코드가 끝난 이후에 작동된다는 것입니다. 이제 결과로 Task 1과 Task 2의 결과로 (1,2), (2,1)이 나올 수 있습니다. 117 | 118 | 여기서, Task2가 Task1이 동작하고 있을 때 기다린다는 것을 보장하기 위해 `await`키워드가 사용됩니다. Task가 actor에 작업을 요청할 때, actor에서 코드가 작동되고 있다면 CPU는 해당 Task를 중지하고 다른 Task를 실행합니다. 그리고 actor가 자유로워지면 다시 해당 Task를 실행합니다. 119 | 120 | ```swift 121 | extension Counter { 122 | func resetSlowly(to newValue: Int) { 123 | value = 0 124 | for _ in 0.. Image? { 143 | if let cached = cache[url] { 144 | return cached 145 | } 146 | 147 | let image = try await downloadImage(from: url) 148 | 149 | // Potential bug: `cache` may have changed. 150 | cache[url] = image 151 | return image 152 | } 153 | } 154 | ``` 155 | 156 | 이미지 다운로드 Actor입니다. 다른 서비스에서 이미지를 다운로드하는 역할입니다. 또한 다운로드한 이미지를 캐시에 저장해 동일한 이미지는 여러번 다운로드 하지 않도록 합니다. 157 | 158 | Actor의 동기화 매커니즘은 한 번에 하나의 작업만 캐시 인스턴스에 접근하도록 보장하므로 캐시가 손상될 수 있는 경우는 없다고 생각할테지만, `await`키워드가 문제입니다. `await`으로 인해 코드 실행이 중지될 때마다 프로그램의 다른 코드가 실행될 수 있고, 실행되는 다른 함수로 인해 이상이 생길 수 있습니다. 159 | 160 | ![R1280x0](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/R1280x0.png) 161 | 162 | 예를 들어 동일한 이미지를 동시에 가지고 오려고 하는 2개의 concurrent task가 있다고 합시다. 첫 번째는 캐시에 이미지가 없음을 확인하고 서버에서 웃는 고양이 이미지 다운로드를 시작한 뒤 다운로드하는 동안 CPU는 해당 작업을 중지하고 다른 함수를 시작합니다. 이렇게 첫 번째 작업이 웃는 고양이 이미지를 다운로드하는 동안 새로운 우는 고양이 이미지가 서버에 올라옵니다. 그리고 두 번째 이미지 다운로드 작업이 실행됩니다. 163 | 164 | ![111](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/111.png) 165 | 166 | 두 번째 작업이 서버에서 이미지를 가져오는데 첫 번째 작업이 완료가 되지 았았기 때문에 아직 캐시에 이미지가 저장되지 않아 동일한 URL이미지 다운로드를 시작합니다. 물론 이 경우에도 CPU는 작업을 중단합니다. 하지만 결과를 보았을 때 동일한 URL 주소인데 다른 고양이 이미지가 다운로드되는 것을 알 수 있습니다. `await` 키워드로 인해 버그가 발생한 것입니다. 167 | 168 | 이를 수정하기 위해서는 `await`후에 잘 수행되는지 확인해야합니다. 첫 번째 방법으로는 cache에 이미지가 저장되어있다면, 이미지 저장을 생략하는 방식입니다. 하지만 더 나은 해결 방식은 불필요한 이미지 다운로드를 제거하는 것입니다. 아래 코드는 중복되는 URL인 경우 아예 다운로드하지 못하게 막는 방법입니다. 169 | 170 | ```swift 171 | actor ImageDownloader { 172 | 173 | private enum CacheEntry { 174 | case inProgress(Task) 175 | case ready(Image) 176 | } 177 | 178 | private var cache: [URL: CacheEntry] = [:] 179 | 180 | func image(from url: URL) async throws -> Image? { 181 | if let cached = cache[url] { 182 | switch cached { 183 | case .ready(let image): 184 | return image 185 | case .inProgress(let task): 186 | return try await task.value 187 | } 188 | } 189 | 190 | let task = Task { 191 | try await downloadImage(from: url) 192 | } 193 | 194 | cache[url] = .inProgress(task) 195 | 196 | do { 197 | let image = try await task.value 198 | cache[url] = .ready(image) 199 | return image 200 | } catch { 201 | cache[url] = nil 202 | throw error 203 | } 204 | } 205 | } 206 | ``` 207 | 208 | Actor reentrancy(Actor 재진입) 문제는 `await`키워드로 인해 발생합니다. 따라서 이를 예방하는 방법으로 첫 번째, 동기 코드 내에서만 Actor의 상태변경을 수행하면 됩니다. (동기 함수 내에서 모든 상태변경이 되도록 캡슐화하기) 두 번째 방법은 코드가 중단된 시점에 Actor의 상태가 변할 수 있다는 것을 생각하고, await 이전의 일관성을 다시 복원해야 합니다. 따라서 `await`이후 global state, clocks와 같은 것들을 확인해줍니다. 209 | 210 | > 영상에서는 gloabl state, clocks, timer와 같은 것들을 확인하라고 하는데 왜 저것들을 확인해주어야하는지 이해가 가지 않네요. Actor의 상태가 변했는지 안변했는지 확인하는 것이 중요한 것 아닌가? 211 | 212 | ### Actor isolation 213 | 214 | Actor isolation은 Actor 타입의 동작의 기본입니다. 아까 Actor 외부에서 일어나는 비동기 함수들과 상호작용하는 Actor 타입의 isolation에 대해서 알아보았다면 이번에는 프로토콜 채택, 클로저, 클래스 등 다른 기능들과 어떻게 isolation을 유지하며 상호작용하는지 알아보겠습니다. 215 | 216 | 1. 프로토콜 217 | 218 | ```swift 219 | actor LibraryAccount { 220 | let idNumber: Int 221 | var booksOnLoan: [Book] = [] 222 | } 223 | 224 | extension LibraryAccount: Equatable { 225 | static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool { 226 | lhs.idNumber == rhs.idNumber 227 | } 228 | } 229 | ``` 230 | 231 | 먼저, `Equatable`프로토콜을 채택하게 되면 `==(lhs:rhs:)` 함수를 구현해야합니다. 이 함수는 static이므로 인스턴스가 없기 때문에 Actor에 의해 isolated되지 않았습니다. 외부에서 매개변수로 2개의 Actor를 받아 처리하기 때문에 Actor isolation과는 무관합니다. 232 | 233 | ```swift 234 | actor LibraryAccount { 235 | let idNumber: Int 236 | var booksOnLoan: [Book] = [] 237 | } 238 | 239 | extension LibraryAccount: Hashable { 240 | nonisolated func hash(into hasher: inout Hasher) { 241 | hasher.combine(idNumber) 242 | } 243 | } 244 | ``` 245 | 246 | 하지만 `Hashable`프로토콜을 채택하게 될 경우, hash함수를 구현해야 하는데 이 함수는 Actor 외부에서 호출할 수 있지만 내부의 값을 바꿀 수 있습니다. 즉, isolation하게 유지해야합니다. 하지만 `Hashable` 프로토콜에 이미 구현되어 있는 함수이기 때문에 비동기함수로 다시 구현할 수도 없습니다. 이럴 경우 `nonisolated`키워드를 앞에 붙입니다. 해당 키워드는 Actor 외부에 있는 것처럼 처리하여 Actor의 mutable state를 참조할 수 없게 합니다. 위 코드에선 `idNumber`는 let선언되어 immutable state이므로 오류가 나지 않지만 만일 booksOnLoon을 참조하게 된다면 컴파일 오류가 납니다. 247 | 248 | ![스크린샷 2022-10-12 오전 2.08.19](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-10-12%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.08.19.png) 249 | 250 | > 🤔 바로 위에선 actor isolation을 지키기 위해선 async 함수여야한다고 하는데 이전에 생성한 increment 함수는 async 함수였나? 아니었는데 거기선 왜 컴파일 오류가 나지 않았을까? 251 | > 함수 자체에서 actor 프로퍼티에 접근해 값을 수정하는지 안하는지를 보는 것 같습니다. 252 | 253 | 2. 클로저 254 | 255 | 클로저는 actor isolation을 지킬 수 있습니다. 256 | 257 | ```swift 258 | extension LibraryAccount { 259 | func readSome(_ book: Book) -> Int { ... } 260 | 261 | func read() -> Int { 262 | booksOnLoan.reduce(0) { book in 263 | readSome(book) 264 | } 265 | } 266 | } 267 | ``` 268 | 269 | 위 예제는 하나의 actor 내의 함수를 클로저 내에서 호출한 상황입니다. 이 상황에선 actor isolation합니다. readSome 함수에 `await`도 없고, `read` 함수 자체만으로 이미 isolated합니다. 또한, reduce 고차함수는 순차적으로 작동하기 때문에 안전합니다. 270 | 271 | ```swift 272 | extension LibraryAccount { 273 | func readSome(_ book: Book) -> Int { ... } 274 | func read() -> Int { ... } 275 | 276 | func readLater() { 277 | Task.detached { 278 | await read() 279 | } 280 | } 281 | } 282 | ``` 283 | 284 | 다른 경우를 살펴보겠습니다. 나중에 읽는 경우를 생각해봅시다. `detached` Task를 통해 Actor가 실행하고 있는 다른 작업도 동시에 실행시킵니다. 따라서 클로저가 하나의 Actor 내에서 실행되고 있지 않기 때문에 Data Race가 발생할 수 있습니다. 따라서 이런 경우 클로저는 Actor와 nonisolated합니다. read()메소드(isolated한 함수)를 호출하기 위해서는 `await`키워드를 넣어야 합니다. 285 | 286 | 287 | 288 | 퍼런스 타입 289 | 290 | 291 | 292 | - 레퍼런스 타입 293 | 294 | 295 | 296 | - 레퍼런스 타입 297 | - protocol을 채택할 수 있고, extension을 통해 확장 가능 298 | - 프로퍼티, 메소드, initializer, subscript 등 제공 299 | - 300 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:11_concurrency_swift5.5_basic.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.11 (화) 6 | 7 | ps) 이 글을 작성하다가 swift-book에서 swift 문법 오류 발견해서 [PR](https://github.com/apple/swift-book/pull/45)날림. 😅 😆 8 | 9 | 10 | ### **Concurrency - Swift5.5 이전** 11 | 12 | Swfit언어는 비동기 처리와 병렬 처리를 위한 작업을 도와주도록 설게되었습니다. 기존의 Swift언어에서는 비동기작업을 completion handler를 이용하여 처리했습니다. 하지만 이는 단점이 몇가지가 있습니다.
예를 들어, 이미지이름 리스트를 불러오고 첫번째 이미지를 다운로드한 후, 화면에 보여주는 상황이라면, 13 | 14 | ```swift 15 | listPhotos(inGallery: "Summer Vacation") { photoNames in 16 | let sortedNames = photoNames.sorted() 17 | let name = sortedNames[0] 18 | downloadPhoto(named: name) { photo in 19 | show(photo) 20 | } 21 | } 22 | ``` 23 | 24 | 이미 중첩 클로저를 통해 읽기 어려운 코드가 될뿐더러, 비동기 작업이 더 추가된다면 더 중첩되어 복잡해질 겁니다.
그리고 해당 작업이 어떤 스레드에서 작업될 지 모르기 때문에 UI와 관련된 작업은 `DispatchQueue.main`으로 감싸주어야 합니다. 25 | 26 | ```swift 27 | listPhotos(inGallery: "Summer Vacation") { photoNames in 28 | let sortedNames = photoNames.sorted() 29 | let name = sortedNames[0] 30 | downloadPhoto(named: name) { photo in 31 | DispatchQueme.main { // ✅ 32 | show(photo) 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | 한가지 더, 해당 작업이 두번 쓰이기 때문에 함수화시켜야 할 경우, 모든 상황(에러가 난 경우)에서 completion handler를 반환했는지 고려해야합니다. 아래 코드는 에러처리를 고려하지 않았습니다. 만일 고려한다면 `listPhotos`, `downloadPhoto`의 completion handler가 `Result`타입으로 리턴되게 수정하고 해당 오류에 맞게 `showPhoto`의 completion handler를 반환해주어야 합니다. 여기서 *개발자의 실수로 인해 발생할 수 있는 오류*가 다수 생깁니다. 39 | 40 | ```swift 41 | func showPhoto(completion: @escpaing ((UIImage) -> Void)) { 42 | listPhotos(inGallery: "Summer Vacation") { photoNames in 43 | let sortedNames = photoNames.sorted() 44 | let name = sortedNames[0] 45 | downloadPhoto(named: name) { photo in 46 | completion(photo) // ✅ 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | 마지막으로, retain cycle이 발생할 가능성이 있습니다. 클로저 내부에 self 프로퍼티를 이용해서 접근할 때 어느 쓰레드에서 접근하게 될 것인지 고려해야 합니다. escaping closure의 경우 일반 클로저와는 다르게, 레퍼런스 카운팅이 되기 때문에 추가적인 메모리 관리를 해주어야합니다. `[weak self]`를 통해 레퍼런스 카운팅을 제거하여 retain cycle 발생을 예방해야 하지만, 무분별한 `[weak self]`의 사용은 런타임 오버헤드를 일으키고, 옵셔널을 체크해줘야 하는 번거로움이 생깁니다. 53 | 54 | ```swift 55 | listPhotos(inGallery: "Summer Vacation") { [weak self] photoNames in // ✅ 56 | let sortedNames = self?.photoNames.sorted() ?? [defaultImageName] // ✅ 57 | let name = sortedNames[0] 58 | downloadPhoto(named: name) { [weak self] photo in // ✅ 59 | DispatchQueme.main { [weak self] // ✅ 60 | self?.show(photo) // ✅ 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | completion handler의 단점을 정리하면 아래와 같은 것들이 있습니다. 67 | 68 | - 중첩클로저로 인해 코드의 가독성 저하 69 | - 개발자의 실수로 인해 발생할 수 있는 다수의 오류 상황 존재 70 | - self capture -> retain cycle 71 | - 에러 핸들링 72 | 73 | 74 | 75 | ### 비동기 함수 선언 및 호출하기 76 | 77 | 비동기 함수(asynchrounous function/method)는 실행 도중 멈출 수 있는 특별한 종류의 함수입니다. 타입을 반환하거나, 에러를 던지거나, 아예 반환하지 않는 기존 함수와는 조금 다릅니다. 3가지 중 하나를 하긴 하지만, 작업 중간에 멈출 수 있습니다. 78 | 79 | 비동기 함수를 작성할 때는 파라미터 뒤에 `async` 키워드를 사용합니다. 그리고 값을 반환할 경우엔 타입을 화살표(->) 뒤에 작성합니다. 그리고 오류를 던질 수 있는 비동기 함수일 경우엔 `async throws`로 작성할 수 있습니다. 80 | 81 | ```swift 82 | func listPhotos(inGallery name: String) async -> [String] { 83 | let result = // ... some asynchronous networking code ... 84 | return result 85 | } 86 | ``` 87 | 88 | 함수의 작동을 중간에 멈추는 키워드는 `await`입니다. 89 | 90 | ```swift 91 | let photoNames = await listPhotos(inGallery: "Summer Vacation") // ⛔️ 92 | let sortedNames = photoNames.sorted() 93 | let name = sortedNames[0] 94 | let photo = await downloadPhoto(named: name) // ⛔️ 95 | show(photo) 96 | ``` 97 | 98 | completion handler의 예제코드를 async/await을 이용하여 코드를 작성해보면 위와 같습니다. 사진들의 이름을 불러오는 작업이 시작되면 해당 줄에서 함수는 멈추고, 작업을 맡고 있던 스레드는 다른 작업을 실행합니다. 모두 불러와졌다면 해다아 줄부터 다시 작업을 시작하여 `photoNames`에 결과를 할당하고 다음 줄로 넘어갑니다. `await`키워드가 붙어있다면, 위와 같이 동작하고 아니라면 일반 함수처럼 동기적으로 동작합니다. 99 | 100 | `await`키워드가 표시된 코드는 해당 작업이 완료되어 반환될 때까지 다음 작업 실행을 멈추고, 기다리라는 의미입니다. 이 뜻은 *yielding the thread*라고도 합니다. 왜냐하면 Swift가 작업이 동작되고 있던 스레드에서 해당 작업을 멈추고 다른 코드를 동작시키기 때문입니다. `await`에서만 실행을 멈출 수 있기 때문에 특정 부분에서만 비동기 함수를 호출할 수 있습니다. 101 | 102 | - 비동기 메소드 및 프로퍼티 내부 103 | - `@main`키워드로 표시된 structure, main, enum 104 | - Unstructured child task 내부 (아래에서 설명) 105 | 106 | ### 비동기 스트림 (Asynchronous Sequeunce) 107 | 108 | `listPhotos(inGallery:)` 함수는 모든 이미지 리스트를 불러온 다음에 다음 작업이 실행됩니다. Swift의 비동기 스트림을 이용하면 이미지 리스트와 같이 collection타입인 경우 하나의 element만 불러올 때까지만 기다리게 할 수 있습니다. 109 | 110 | ```swift 111 | import Foundation 112 | 113 | let handle = FileHandle.standardInput 114 | for try await line in handle.bytes.lines { 115 | print(line) 116 | } 117 | ``` 118 | 119 | `for - await - in` 구문을 통해 각 iteration마다 값이 준비될때까지 멈추고 값이 준비되면 다시 동작하게끔할 수 있습니다. 만약 iteration이 10번이라면 총 10번 멈출 것입니다. `AsyncSequence`프로토콜을 채택한 타입이라면 `for-await-in`구문을 사용할 수 있습니다. 120 | 121 | ### 병렬적으로 비동기 함수 호출하기 122 | 123 | `await`키워드로 비동기 함수를 호출하는 것은 오직 한번에 하나입니다. 즉, caller(비동기함수를 부르는 함수)가 비동기 함수를 부르고 있다면 다음 코드로 넘어가지 않습니다. 예를 들어, 이미지 3개를 동시에 다운받는 상황이리고 합시다. 124 | 125 | ```swift 126 | let firstPhoto = await downloadPhoto(named: photoNames[0]) 127 | let secondPhoto = await downloadPhoto(named: photoNames[1]) 128 | let thirdPhoto = await downloadPhoto(named: photoNames[2]) 129 | 130 | let photos = [firstPhoto, secondPhoto, thirdPhoto] 131 | show(photos) 132 | ``` 133 | 134 | 위와 같이 코드를 작성하면 첫번째 이미지가 다운로드된 후, 두번째 이미지가 다운로드되고, 두번째 이미지가 다운로드된 후 세번째 이미지가 다운로드됩니다. 모두 순차적으로 다운로드가 진행되지만, 이미지 다운로드는 모두 독립적으로, 즉 병렬적으로 진행되어야하는 작업입니다. 135 | 136 | 비동기 함수를 병렬적으로 호출하기 위해서는 `async`키워드를 `let`키워드 앞에 붙입니다. 그리고 해당 상수를 사용할 때 `await` 키워드를 붙입니다. 137 | 138 | ```swift 139 | async let firstPhoto = downloadPhoto(named: photoNames[0]) 140 | async let secondPhoto = downloadPhoto(named: photoNames[1]) 141 | async let thirdPhoto = downloadPhoto(named: photoNames[2]) 142 | 143 | let photos = await [firstPhoto, secondPhoto, thirdPhoto] 144 | show(photos) 145 | ``` 146 | 147 | 시스템 자원이 충분하다면, 이미지 3개 다운로드 작업이 동시에 진행됩니다. 다운로드하는 비동기함수로 인행 실행이 멈추지 않기 때문에 `await`키워드로 표시되지 않습니다. 대신에, photos 상수가 할당될 때, 비동기 함수를 호출하고 3개의 비동기 함수가 모두 완료될 때까지 caller의 실행이 중단됩니다. 148 | 149 | **await과 async-let 정리** 150 | 151 | | | 공통점 | 차이점 | 152 | | --------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 153 | | await | 1. caller는 중단되고 다른 코드가 실행된다.
2. 모두 `await`키워드를 통해 실행 중인 작업이 중단된다. | 1. 다음 코드가 비동기함수의 결과에 의존하고 있을 경우 사용한다.
2. 순차적 진행 | 154 | | async-let | 위와 같음 | 1. 다음 코드가 비동기함수의 결과에 의존하고 있지 않을 경우 사용한다.
2. 병렬적 진행 | 155 | 156 | ### Task and Task Groups 157 | 158 | 비동기적으로 처리해야할 작업을 처리할 때 실행되는 작업의 단위입니다. 모든 비동기 코드들은 task의 일부로서 작동됩니다. 위에서 설명한 `async-let` 키워드는 자동으로 자식 Task를 생성하여 처리합니다. Swift는 개발자가 직접 task group을 생성하고 자식 Task를 삽입할 수 있도록 하여 Task별 우선순위와 cancellation를 컨트롤할 수 있게 해줍니다. 159 | 160 | Task는 계층적(hierarchy)으로 정리됩니다. Task Group내의 각 Task는 같은 부모 Task를 가지고, 각 Task는 자식 Task를 가질 수 있습니다. Task와 Task Group이라는 명시적인 관계가 있기 때문에 이를 *structured concurrency*라고도 부릅니다. Task와 Task Group을 사용함으로서, 개발자의 실수가 조금 생길지라도, 명시적인 관계를 통해 Swift는 컴파일 타임에 오류를 찾아낼 수 있고, `부모 Cancellation -> 자식 Cancellation`와 같은 작업도 할 수 있습니다. 161 | 162 | ```swift 163 | await withTaskGroup(of: Data.self) { taskGroup in 164 | let photoNames = await listPhotos(inGallery: "Summer Vacation") 165 | for name in photoNames { 166 | taskGroup.addTask { await downloadPhoto(named: name) } 167 | } 168 | } 169 | ``` 170 | 171 | **Unstructured Concurrency** 172 | 173 | 부모 Task를 가지지 않는 Task도 존재하는데 이를 *Unstructured Concurrency*라고 합니다. 개발자가 Task를 전부 관리하기 때문에 개발자의 실수로 인해 발생할 오류들이 생깁니다. 현재 actor에서 unstructured concurreny task를 생성하는 방법은 `Task.init(priority: operation:)` initializer입니다. 현재 actor에서 생성하지 않고 다른 actor로 생성하는 방법은 `Task.detached(priority:operation:)` 클래스 메소드를 호출하는 것입니다. *(detatched task*라고 알려져있음) 이 두가지 메소드는 개발자가 상호작용(비동기 메소드 결과 기다리기, 취소하기)할 수 있는 Task를 반환합니다. (Actor는 밑에서!) 174 | 175 | ```swift 176 | let newPhoto = // ... some photo data ... 177 | let handle = Task { 178 | return await add(newPhoto, toGalleryNamed: "Spring Adventures") 179 | } 180 | let result = await handle.value 181 | ``` 182 | 183 | Spring Adeventures라는 갤러리에 새로운 사진을 추가하는 Task를 생성하는 예제입니다. Task에 대해선 문서를 보면서 더 자세히 공부해야겠습니다. 184 | 185 | **Task Cancellation** 186 | 187 | Swift 동시성은 공통의 cancellation 모델을 사용합니다. 각 Task는 적절한 실행시점에서 취소되었는지 체크하고 적절한 방식으로 취소에 응답합니다. 여기서 적절한 실행시점이란 아래의 상황입니다. 188 | 189 | - `CancellationError`와 같은 에러를 던질 때 190 | - nil이나 빈 collection을 반환할 때 191 | - 부분적으로 완료된 작업을 반환할 때 192 | 193 | `cancellation`을 체크하기 위해서는 2가지 방법이 있습니다. Task가 취소될 때 `CancellationError`를 던지는 `Task.checkCancellation()`을 호출하거나 `Task.isCancelled`의 값을 체크해서 취소된 상황을 핸들링하는 것입니다. 예를 들어 갤러리에서 사진을 다운로드하는 Task에서는 부분적인 다운로드를 제거하고 네트워크 연결을 닫는 작업이 있을 것입니다. 194 | 195 | ### Actors 196 | 197 | 이제 개발자들은 프로그램을 동시에 작동할 수 있는 Task 조각으로 쪼갤 수 있습니다. 이 Task 조각들은 각각 독립되어 동시에 작동하여 서로로부터 안전하지만, Task 사이에서 공유되는 자원들은 안전하지 않습니다. Actor는 동시성 코드로부터 공유 자원을 안전하게 관리해줍니다. 198 | 199 | Actor는 레퍼런스 타입입니다. 하지만 클래스와 달리, 한번에 하나의 Task만이 State에 접근하여 변경할 수 있어 여러개의 Task들로부터 하나의 actor 인스턴스에 접근하는 것을 방지합니다. 예를들어, 기온을 기록하는 actor가 있다고 합시다. 200 | 201 | ```swift 202 | actor TemperatureLogger { 203 | let label: String 204 | var measurements: [Int] 205 | private(set) var max: Int 206 | 207 | init(label: String, measurement: Int) { 208 | self.label = label 209 | self.measurements = [measurement] 210 | self.max = measurement 211 | } 212 | } 213 | ``` 214 | 215 | `TemperatureLogger` actor는 외부에서 접근 가능한 프로퍼티 `label`과 `measurements`를 가지고 있고 `max`와 같이 actor 내부에서만 접근하여 수정가능한 프로퍼티가 있습니다. 216 | 217 | 이제 class와 structure과 같은 initializer를 이용해 actor 인스턴스를 생성합니다. Actor의 프로퍼티와 메소드에 접근할 땐, `await` 키워드를 이용하여 정지할 포인트를 표시합니다. 218 | 219 | ```swift 220 | let logger = TemperatureLogger(label: "Outdoors", measurement: 25) 221 | print(await logger.max) 222 | // Prints "25" 223 | ``` 224 | 225 | 예제에서는, `logger.max`부분에 정지하도록 했습니다. 왜냐하면 actor는 한번에 하나의 Task만이 이 값에 접근가능하도록 해야하기 때문입니다. 만약 다른 Task가 이미 logger와 상호작용하고 있다면, 이 코드는 프로퍼티에 접근 가능할 때까지 기다립니다. 226 | 227 | 반대로, Actor내부의 코드는 `await`키워드를 사용해서 Actor 프로퍼티에 접근하지 않습니다. 예를 들어, 기온을 새로 업데이트하는 메소드가 있다고 합시다. 228 | 229 | ```swift 230 | extension TemperatureLogger { 231 | func update(with measurement: Int) { 232 | measurements.append(measurement) 233 | if measurement > max { 234 | max = measurement 235 | } 236 | } 237 | } 238 | ``` 239 | 240 | `update(with:)`메소드는 이미 actor에 있기 때문에, `await`키워드를 사용해 `max` 프로퍼티에 않아도 됩니다. 이 메소드는 왜 Actor가 한번에 하나의 Task만이 접근가능하게 하는 지에 대해서도 알려줍니다. 1번 Task는 `measurements`프로퍼티에 새로운 기온을 저장하고 최고 기온을 업데이트하는 작업입니다. 1번 Task가 실행하는 도중, `measurements`프로퍼티만 새로 업데이트가 되었을 때, 2번 Task에서 `max`프로퍼티에 접근해 읽는 상황이 있다고 합시다. 그리고 1번 Task가 마무리되어, 새로운 `max`프로퍼티를 갱신했다면, 1번 Task와 2번 Task는 다른 `max`프로퍼티를 가지게 된 상황이 생길뿐더러, 2번 Task는 유효하지 않은 최고 기온값을 가지게 됩니다. 이 문제를 예방하기 위해 actor는 한번에 하나의 Task를 허용해줍니다. `update(with:)`메소드는 그 어떤 정지 포인트(suspension point)를 가지고 있지 않기 때문에, 그 어떤 코드도 업데이트 도중 데이터에 접근할 수 없습니다. (만일 `await`를 가지고 있다면 업데이트도 중간에 멈출 것이고, actor는 다른 Task한테 열린 상태가 될 것입니다. - 추측) 241 | 242 | 만약 actor외부에서 아래와 같이 프로퍼티에 접근한다면, 컴파일 타임 에러가 납니다. 243 | 244 | ```swift 245 | print(logger.max) // Error ☠️ 246 | ``` 247 | 248 | Swift는 Actor내부의 코드만이 Actor의 지역변수에 바로 접근을 보장합니다. 이것을 *actor isolation*이라고도 합니다. (용어가 중요한가?라고 할 수 있지만 나중에 언제 나올지 모르니 알아는 두자. 까먹을 수 있으니 종종 읽어보자) 249 | 250 | > actor isoliation: swift가 actor외부에서 actor 내부 지역변수에 바로 접근이 불가능하고 내부에서만 접근이 가능한 것을 보장하는 상황 (외부에서 접근하기 위해선 await키워드를 추가해야 한다.) 251 | 252 | ### Sendable Types 253 | 254 | 라는 것도 있는데 2022-09-12 가장 최근에 문서에 추가되었습니다. 이것은 위 프로퍼티와 메소들에 대해 더 공부해보고 다시 읽어보려고 합니다! 255 | 256 |
257 | 258 | 259 | 260 | 261 | 262 | - https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html 263 | - https://docs.swift.org/swift-book/RevisionHistory/RevisionHistory.html 264 | - https://tech.devsisters.com/posts/crunchy-concurrency-swift/ 265 | -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:02_swift_naming.md: -------------------------------------------------------------------------------- 1 | ### Today I Learned 2 | 3 | 4 | 5 | - [Fundamental](#fundamental) 6 | - [사용 시점의 명확성이 가장 중요하다.](#사용-시점의-명확성이-가장-중요하다) 7 | - [간결함보다 명료함이 더 중요하다.](#간결함보다-명료함이-더-중요하다) 8 | - [주석 달기](#주석을-달아라) 9 | - [Naming](#naming) 10 | - [코드를 읽는 사람이 모호하다고 느끼지 않도록 필요한 단어를 모두 포함시키기](#코드를-읽는-사람이-모호하다고-느끼지-않도록-필요한-단어를-모두-포함시키기) 11 | - [불필요한 말은 생략하기](#불필요한-말은-생략하기) 12 | - [변수, 매개변수, 관련 타입의 네이밍은 그들의 타입보다는 역할과 관련되기](#변수-매개변수-관련-타입의-네이밍은-그들의-타입보다는-역할과-관련되기) 13 | - [매개 변수의 역할을 명확히 하기 위해 약한 유형 정보를 보정합니다](#매개-변수의-역할을-명확히-하기-위해-약한-유형-정보를-보정합니다) 14 | - [Strive for Fluent Usage](#strive-for-fluent-usage) 15 | - [문법적인 영어 구문을 선호하기](#문법적인-영어-구문을-선호하기) 16 | - [Factory 메소드는 "make"로 시작하기](#factory-메소드는-make로-시작하기) 17 | - [initializer, factory 메소드의 매개변수는 구문을 만들지 말기](#initializer-factory-메소드의-매개변수는-구문을-만들지-말기) 18 | - [사이드 이펙트에 따른 메소드명 짓기](#사이드-이펙트에-따른-메소드명-짓기) 19 | - [Boolean 메소드와 프로퍼티는 값에 변화가 일어나지 않는다면, 문장으로 읽혀야합니다.](#boolean-메소드와-프로퍼티는-값에-변화가-일어나지-않는다면-문장으로-읽혀야합니다) 20 | - [무엇인가를 기술하는 Protocol은 명사로 읽혀야 합니다.](#무엇인가를-기술하는-protocol은-명사로-읽혀야-합니다) 21 | - [기능을 설명하는 Protocol은 "able", "ible", 또는 "ing" 접미사를 이용해서 네이밍 짓기](#기능을-설명하는-protocol은-able-ible-또는-ing-접미사를-이용해서-네이밍-짓기) 22 | - [이외의 다른 프로퍼티, 변수, 상수의 이름은 명사로 읽혀야한다.](#이외의-다른-프로퍼티-변수-상수의-이름은-명사로-읽혀야한다) 23 | - [Conventions](#conventions) 24 | - [Free Function보다는 메소드와 프로퍼티를 선호하기](#free-function보다는-메소드와-프로퍼티를-선호하기) 25 | - [대소문자 표기법 따르기](#대소문자-표기법-따르기) 26 | - [메소드들의 기능들이 같다면 기본 이름 공유하기](#메소드들의-기능들이-같다면-기본-이름-공유하기) 27 | - [Parmeter(매개 변수)과 Argument (인수)](#parmeter매개-변수과-argument-인수) 28 | 29 | 30 | ---- 31 | 32 | 2022.10.02 (일) 33 | 34 |
35 | 36 | 개발하면서 프로퍼티와 메소드의 네이밍을 고민하는 시간이 개발 시간의 거의 절반 이상을 차지합니다. 그럼에도 불구하구 최악의 네이밍이 탄생하게 되네요. 37 | 38 | ```swift 39 | 40 | extension UIButton { 41 | func setBasicProfileImageWhenNilAndEmpty(with urlString: String?) { 42 | if let urlString = urlString, urlString.isEmpty == false { 43 | self.imageView?.kf.indicatorType = .activity 44 | let url = URL(string: urlString) 45 | self.kf.setImage(with: url, for: .normal, 46 | options: [.forceTransition]) 47 | } else { 48 | // ... 49 | } 50 | } 51 | } 52 | 53 | ``` 54 | 55 | 버튼의 이미지를 설정하는 Extension 코드인데 setBasicProfileImageWhenNilAndEmpty ...?
제가 만든 메소드이긴 하지만 너무 길고.. 다른 개발자들이 잘 이해할 수 있을 지..!? 😭 56 | 그 기준을 [Swift Documentation API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/#naming) 문서를 읽으면서 찾아봐요. 57 | 58 | ---- 59 | 60 | ### Fundamental 61 | 62 | #### 사용 시점의 명확성이 가장 중요하다. 63 | 64 | 프로퍼티와 메소드들은 한 번만 선언되지만 반복적으로 사용되므로 API를 설계할 때는 그것이 명확하고 정확해야 한다는데, 사실 이 말은 그렇게 와닿지 않아요. 명확한 예시가 없어서.. 65 | 66 | #### 간결함보다 명료함이 더 중요하다. 67 | 68 | 문자는 가장 적게 사용하여 코드에 대한 이해를 하는 것이 중요합니다. 스위프트의 메소드를 만들 때 아래와 같이 파라미터를 이용해서 더 명료하게 네이밍을 할 수 있습니다. 69 | 70 | ```swift 71 | func insert(at element: Int) { } 72 | func removeAll() {} 73 | ``` 74 | 75 | 하지만 간결하게 작성하기 위해서 명료함을 버리면 안돼요! 무조건 명료함이 우선입니다. 76 | 77 | #### 주석을 달아라 78 | 79 | 모든 코드에 대해 주석을 작성합니다. 문서(주석)를 작성함으로써 얻은 통찰력은 API 설계에도 엄청난 영향을 미칠 수 있으니 미루지 않기!
근데 주석은 최대한 작성하지 않고 메소드명과 프로퍼티명만으로 메소드의 동작을 이해하게 만드는 것이 가장 좋다라고 <클린 코드>책에 쓰여져있는데.. 그럼 뭐가 맞는걸까!? 🤔 80 | 81 | 애플 문서를 따르는 게 좋겠어요. 82 | 83 | 1. 주석을 작성함으로써 얻은 통찰력을 통해 API 설계를 더 명료하게 할 수 있다! 84 | - 만일 주석을 달기 어려운 API가 있다면, 잘못된 API를 짠 것임 85 | 2. 나중에 합류한 개발자들이 코드에 대한 이해를 더 빠르게 할 수 있다! 86 | 3. 애플이 작성한 프로퍼티와 메소드들도 대부분 주석이 달려 있다! (이것도 중요하져.. best practice를 찾아가기 위해서는 애플 코드에서 영감을 받아야 해요) 87 | 88 | ```swift 89 | @frozen public struct String { 90 | 91 | /// Creates an empty string. 92 | /// 93 | /// Using this initializer is equivalent to initializing a string with an 94 | /// empty string literal. 95 | /// 96 | /// let empty = "" 97 | /// let alsoEmpty = String() 98 | @inlinable public init() 99 | ``` 100 | 101 | 그럼 주석은 어떤 식으로 작성해야할까? 102 | 103 | 1. 주석 시작은 summary로! 104 | 105 | **summary 규칙들** 106 | 107 | - .으로 끝내구 완벽한 문장을 만들지 말기 108 | - 메소드가 무슨 기능을 하는지 설명하고 리턴값을 명시하기 109 | 110 | ```swift 111 | /// Inserts `newHead` at the beginning of `self`. 112 | mutating func prepend(_ newHead: Int) 113 | 114 | /// Returns a `List` containing `head` followed by the elements 115 | /// of `self`. 116 | func prepending(_ head: Element) -> List 117 | 118 | /// Removes and returns the first element of `self` if non-empty; 119 | /// returns `nil` otherwise. 120 | mutating func popFirst() -> Element? 121 | ``` 122 | 123 | - subscript가 접근하는 것이 무엇인지 설명하기 124 | 125 | ```swift 126 | /// Accesses the `index`th element. 127 | subscript(index: Int) -> Element { get set } 128 | ``` 129 | 130 | - initializer가 생성하는 것이 무엇인지 설명하기 131 | 132 | ```swift 133 | /// Creates an instance containing `n` repetitions of `x`. 134 | init(count n: Int, repeatedElement x: Element) 135 | ``` 136 | 137 | - 선언된 엔티티가 무엇을 하는지 설명하기 138 | 139 | ```swift 140 | /// A collection that supports equally efficient insertion/removal 141 | /// at any position. 142 | struct List { 143 | 144 | /// The element at the beginning of `self`, or `nil` if self is 145 | /// empty. 146 | var first: Element? 147 | ... 148 | ``` 149 | 150 | 2. 추가 정보는 Blank line으로 구분하기 151 | 152 | ```swift 153 | /// Writes the textual representation of each ← Summary 154 | /// element of `items` to the standard output. 155 | /// ← Blank line 156 | /// The textual representation for each item `x` ← Additional discussion 157 | /// is generated by the expression `String(x)`. 158 | /// 159 | /// - Parameter separator: text to be printed ⎫ 160 | /// between items. ⎟ 161 | /// - Parameter terminator: text to be printed ⎬ Parameters section 162 | /// at the end. ⎟ 163 | /// ⎭ 164 | /// - Note: To print without a trailing ⎫ 165 | /// newline, pass `terminator: ""` ⎟ 166 | /// ⎬ Symbol commands 167 | /// - SeeAlso: `CustomDebugStringConvertible`, ⎟ 168 | /// `CustomStringConvertible`, `debugPrint`. ⎭ 169 | public func print( 170 | _ items: Any..., separator: String = " ", terminator: String = "\n") 171 | ``` 172 | 173 | 174 | 175 | ---- 176 | 177 | 2022.10.02 (일) 178 | 179 |
180 | 181 | ### Naming 182 | 183 | #### 코드를 읽는 사람이 모호하다고 느끼지 않도록 필요한 단어를 모두 포함시키기 184 | 185 | at 파라미터를 통해 x를 제거하는 것이 아니라 제거할 요소의 위치를 작성해야한다는 사실을 암시할 수 있습니다. 186 | 187 | ```swift 188 | extension List { 189 | public mutating func remove(at position: Index) -> Element 190 | } 191 | 192 | employees.remove(at: x) // ✅ 193 | employees.remove(x) // ☠️ unclear: are we removing x? 194 | ``` 195 | 196 | #### 불필요한 말은 생략하기 197 | 198 | 더 명확하게 하기 위한 의도는 좋지만, 중복된 정보를 주는 단어들은 생략해야 합니다. 애매함을 피하기 위해 정보를 반복해야 하는 경우도 있지만 일반적으론 매개변수를 통해 역할을 설명하는 단어를 사용하는 것이 더 좋다구 하네용
아래 예시에선 Element가 특별한 내용을 전달하지 않고 cancelButton과 중복되기 때문에 생략해주어야 해요. 199 | 200 | ```swift 201 | public mutating func remove(_ member: Element) -> Element? 202 | allViews.remove(cancelButton) // ✅ 203 | 204 | public mutating func removeElement(_ member: Element) -> Element? 205 | allViews.removeElement(cancelButton) // ☠️ 206 | ``` 207 | 208 | #### 변수, 매개변수, 관련 타입의 네이밍은 그들의 타입보다는 역할과 관련되기 209 | 210 | ```swift 211 | var string = "Hello" // ☠️ 212 | protocol ViewController { 213 | associatedtype ViewType : View // ☠️ 214 | } 215 | class ProductionLine { 216 | func restock(from widgetFactory: WidgetFactory) // ☠️ 217 | } 218 | 219 | var greeting = "Hello" // ✅ 220 | protocol ViewController { 221 | associatedtype ContentView : View // ✅ 222 | } 223 | class ProductionLine { 224 | func restock(from supplier: WidgetFactory) // ✅ 225 | } 226 | ``` 227 | 228 | 만일, 역할과 타입과 일치해서 네이밍을 하게 된다면 뒤에 더 추가 설명을 붙여주라고 합니다.
아래 예시에서는 Protocol을 붙여주었네요. 229 | 230 | ```swift 231 | protocol Sequence { 232 | associatedtype Iterator : IteratorProtocol 233 | } 234 | protocol IteratorProtocol { ... } 235 | 236 | ``` 237 | 238 | #### 매개 변수의 역할을 명확히 하기 위해 약한 유형 정보를 보정합니다 239 | 240 | 위에서 불필요한 말은 생략하라고 했는데 여기선, 약간의 보정이 필요하다고 하네요. 매개변수만으로는 설명이 모호해지는 경우엔 역할을 설명하는 명사를 붙입니다. 241 | 242 | ```swift 243 | func add(_ observer: NSObject, for keyPath: String) 244 | grid.add(self, for: graphics) // ☠️ 245 | 246 | func addObserver(_ observer: NSObject, forKeyPath path: String) 247 | grid.addObserver(self, forKeyPath: graphics) // ✅ 248 | ``` 249 | 250 | ### Strive for Fluent Usage 251 | 252 | #### 문법적인 영어 구문을 선호하기 253 | 254 | ```swift 255 | x.insert(y, at: z) // x insert y at z 256 | x.subViews(havingColor: y) // x's subviews having color y 257 | 258 | x.insert(y, position: z) // ☠️ 259 | x.subViews(color: y) // ☠️ 260 | ``` 261 | 262 | 1,2번째 매개변수 이후엔 fluent하지 않아도 된다고 합니당 의미에 영향을 미치지 않는다면은! 263 | 264 | ```swift 265 | AudioUnit.instantiate( 266 | with: description, 267 | options: [.inProcess], completionHandler: stopProgressBar) 268 | ``` 269 | 270 | #### Factory 메소드는 "make"로 시작하기 271 | 272 | 라는데 완전 처음 알았네요. 보통 "create", "init"을 많이 썼는데 새롭군요 273 | 274 | #### initializer, factory 메소드의 매개변수는 구문을 만들지 말기 275 | 276 | 문장을 만들지 말고, 무엇이 필요한 지 적어야 합니다.
277 | 278 | Color는 red, green, blue가 필요하고, widget은 gears, spindles가 필요하고, Link는 target이 필요합니다. 만일 구문을 만들어버린다면 오히려 혼란을 야기할 수 있어요. 279 | 280 | ```swift 281 | // ✅ 좋은 케이스 282 | let foreground = Color(red: 32, green: 64, blue: 128) 283 | let newPart = factory.makeWidget(gears: 42, spindles: 14) 284 | let ref = Link(target: destination) 285 | 286 | // ☠️ 안좋은 케이스 287 | let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128) 288 | let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14) 289 | let ref = Link(to: destination) 290 | 291 | ``` 292 | 293 | #### 사이드 이펙트에 따른 메소드명 짓기 294 | 295 | 사이드 이펙트가 무엇일까요? 사이드 이펙트는 해당 프로퍼티를 이용해 일어난 어떤 일을 의미합니다. 프로퍼티 x를 변화시키는 메소드를 수행한다면 이것은 사이드 이펙트가 있는 메소드입니다. 프로퍼티 x를 출력하는 것 또한 사이드 이펙트가 있는 메소드 입니다. 사이드 이펙트가 없는 메소드의 네이밍은 명사구로 읽히면 좋습니다. 296 | 297 | 사이드 이펙트 있는 메소드 ex) print(x), x.sort(), x.append(y)
사이드 이펙트 없는 메소드 ex) x.distance(to: y), i.successor() 298 | 299 | 값에 변화가 일어나는 메소드는 동사, 일어나지 않는다면 "ed"나 "ing"를 붙여 차이점을 보여줍니다. "ed"를 선호하되, 문법적으로 말이 안된다면 "ing"를 붙이라고 하네요. 300 | 301 | 값에변화가 일어나는 메소드 ex) x.sort(), x.append(y)
값에 변화가 일어나지 않는 메소드 ex) z = x.sorted(), z = x.appending(y) 302 | 303 | #### Boolean 메소드와 프로퍼티는 값에 변화가 일어나지 않는다면, 문장으로 읽혀야합니다. 304 | 305 | ex) x.isEmpty(), line1.intersects(line2) 306 | 307 | #### 무엇인가를 기술하는 Protocol은 명사로 읽혀야 합니다. 308 | 309 | ex) Collection 310 | 311 | #### 기능을 설명하는 Protocol은 "able", "ible", 또는 "ing" 접미사를 이용해서 네이밍 짓기 312 | 313 | ex) Equatable, ProgressReporting 314 | 315 | #### 이외의 다른 프로퍼티, 변수, 상수의 이름은 명사로 읽혀야한다. 316 | 317 | ### Conventions 318 | 319 | #### Free Function보다는 메소드와 프로퍼티를 선호하기 320 | 321 | 아래와 같은 특별한 케이스 말고는 메소드와 프로퍼티를 선호하라고 하는데 Free Function이 무엇일까? 멤버가 없는 함수를 말합니다. 아래 함수들은 어디에도 속하지 않는 함수들이에요. 그렇기에 어디서든 사용할 수 있습니다. 322 | 323 | ```swift 324 | min(x, y, z) 325 | print(x) 326 | sin(x) 327 | ``` 328 | 329 | #### 대소문자 표기법 따르기 330 | 331 | 타입과 프로토콜 빼고는 모두 lowerCamelCase!!
그치만 예외도 있는듯.. utf8나 id 같이 미국 영어에서 일반적으로 대문자로 표시되는 것들이 예외입니다 332 | 333 | ```swift 334 | var utf8Bytes: [UTF8.CodeUnit] 335 | var isRepresentableAsASCII = true 336 | var userSMTPServer: SecureSMTPServer 337 | ``` 338 | 339 | #### 메소드들의 기능들이 같다면 기본 이름 공유하기 340 | 341 | ```swift 342 | // ✅ 좋은 케이스 343 | extension Shape { 344 | func contains(_ other: Point) -> Bool { ... } 345 | func contains(_ other: Shape) -> Bool { ... } 346 | func contains(_ other: LineSegment) -> Bool { ... } 347 | } 348 | ``` 349 | 350 | 아래와 같이 Generic 타입으로 선언해도 좋아요. 351 | 352 | ```swift 353 | // ✅ 좋은 케이스 354 | extension Collection where Element : Equatable { 355 | func contains(_ sought: Element) -> Bool { ... } 356 | } 357 | ``` 358 | 359 | 완전 다른 기능을 할 때에는 같으면 안돼요. 360 | 361 | ```swift 362 | // ☠️ 안 좋은 케이스 363 | extension Database { 364 | /// Rebuilds the database's search index 365 | func index() { ... } 366 | 367 | /// Returns the `n`th row in the given table. 368 | func index(_ n: Int, inTable: TableID) -> TableRow { ... } 369 | } 370 | ``` 371 | 372 | 리턴 타입을 오버로드하는 행위는 피해야 해요! 373 | 374 | ```swift 375 | // ☠️ 안 좋은 케이스 376 | extension Box { 377 | func value() -> Int? { ... } 378 | func value() -> String? { ... } 379 | } 380 | ``` 381 | 382 | #### Parmeter(매개 변수)과 Argument (인수) 383 | 384 | default가 없는 매개 변수를 앞으로 배치시켜 메소드가 호출되는 것이 안정적이 되도록 하라구 해요.
인수를 제대로 구분하지 못할 경우엔 인수 생략!.
타입 변환하는 메소드의 첫번째 인수는 생략!
첫 번째 인수는 상황에 따라 전치사로 하거나 생략하기! 385 | 386 | ```swift 387 | a.moveTo(x: b, y: c) 388 | a.fadeFrom(red: b, green: c, blue: d) 389 | 390 | x.addSubview(y) // 문법적 구문 391 | 392 | view.dismiss(animated: false) // 문법적 구문이 아닐경우엔 label을 붙여야 함 393 | ``` 394 | 395 |
396 | 397 | 398 |
399 | 400 |
401 | 402 | - https://papago.naver.net/website?locale=ko&source=en&target=ko&url=https%3A%2F%2Fwww.swift.org%2Fdocumentation%2Fapi-design-guidelines%2F%23naming 403 | -------------------------------------------------------------------------------- /TIL/2022/WWDC22_swift_charts_raise_the_bar.md: -------------------------------------------------------------------------------- 1 | ### WWDC22 Swift Charts: Raise the bar 2 | 3 | ---- 4 | 5 | 6 | 7 | ## Marks and composition
마크와 마크 구성 8 | 9 | ![스크린샷 2022-11-01 오후 10.20.24](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.20.24.png) 10 | 11 | Mark는 데이터를 나타내는 그래픽 요소입니다. 위 차트는 막대 마크이고 여섯 개의 마크가 있으며 각각 팬케이크의 유형과 판매량을 나타냅니다. 12 | 13 |
14 | 15 | ![스크린샷 2022-11-01 오후 10.23.26](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.23.26.png) 16 | 17 | 코드를 살펴보겠습니다. 위 코드는 단일 차트를 표현할 때 가장 적절한 유형입니다. 차트의 제목과 빈 차트를 볼 수 있습니다. 18 | 19 |
20 | 21 | ![스크린샷 2022-11-01 오후 10.24.43](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.24.43.png) 22 | 23 | 차트에 마크를 추가할 수 있습니다. 차트의 형태는 해당 뷰에 딱 맞는 형태로 들어갑니다. 24 | 25 |
26 | 27 | ![스크린샷 2022-11-01 오후 10.26.32](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.26.32.png) 28 | 29 | 새 막대 마크를 추가하면 두 번째 막대가 등장합니다. 이것을 반복하면 막대를 늘릴 수 있습니다. 30 | 31 |
32 | 33 | ![스크린샷 2022-11-01 오후 10.27.35](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.27.35.png) 34 | 35 | 차트에 들어갈 콘텐츠를 구조체나 튜플 배열을 통해 제공할 수 있습니다. ForEach를 사용해서 각 요소의 값으로 막대 마크를 만들 수 있습니다. 36 | 37 |
38 | 39 | ![스크린샷 2022-11-01 오후 10.29.46](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.29.46.png) 40 | 41 | 만약 ForEach가 차트의 유일한 콘텐츠일 경우 아래와 같이 단축시킬 수도 있습니다. 42 | 43 |
44 | 45 | 여러 SwiftUI modifier를 이용해서 마크에 활용할 수 있습니다. 46 | 47 | ![스크린샷 2022-11-01 오후 10.30.05](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.30.05.png) 48 | 49 | `foregroundStyle`을 이용해서 막대 색을 설정할 도 있고 50 | 51 |
52 | 53 | ![스크린샷 2022-11-01 오후 10.32.32](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.32.32.png) 54 | 55 | `accessibilityLabel`과 `accessibilityValue` modifier를 이용하여 VoiceOver 사용자에게도 차트 정보를 제공할 수 있습니다. 56 | 57 |
58 | 59 | Mark의 종류도 여러가지가 있습니다. 아래는 날짜가 x축이고 판매량이 y축인 차트입니다. 60 | 61 | ![스크린샷 2022-11-01 오후 10.37.20](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.37.20.png) 62 | 63 | 먼저, 막대 마크를 이용해 차트를 그릴 수 있습니다. 64 | 65 |
66 | 67 | ![스크린샷 2022-11-01 오후 10.38.01](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.38.01.png) 68 | 69 | 그리고 BarMark를 LineMark로 수정한다면 꺽은선 차트를 그릴 수 있습니다. 70 | 71 |
72 | 73 | ![스크린샷 2022-11-01 오후 10.41.21](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.41.21.png) 74 | 75 | 두가지 데이터를 하나의 차트로 그렸습니다. 튜플 배열을 이용해서 Chart 내에 2개의 콘텐츠를 삽입했습니다. 각 마크는 `foregroundStyle`을 통해 색깔을 구분했습니다. 시스템은 각 마크가 구분될 수 있도록 색깔을 자동으로 지정해줍니다. (색맹인 사용자도 구분 가능) 각 색상이 의미하는 legend가 추가됩니다. 그리고 `symbol(by:)` modifier를 통해 선에 기호를 추가했습니다. 마지막으로 선이 부드러워 보이도록 보간법을 이용해서 꺽은선을 곡선으로 표현했습니다. 76 | 77 |
78 | 79 | ![스크린샷 2022-11-01 오후 10.45.30](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.45.30.png) 80 | 81 | 이번엔 막대 차트를 그렸습니다. (stacked bar chart) 기본 막대 차트는 두 도시의 총 판매량을 쉽게 파악할 수 있지만 두 도시를 비교하기에는 적절하지 않기에 `position(by:)` modifier를 추가하여 두 도시의 판매량을 쉽게 구분지었습니다. 82 | 83 |
84 | 85 | ![스크린샷 2022-11-01 오후 10.51.32](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.51.32.png) 86 | 87 | 막대 마크와 꺽은선 마크 이외에도 점, 영역, 괘선, 직사각형 마크 등이 있습니다. 이러한 마크들을 결합하면 복잡한 차트가 탄생합니다. 88 | 89 |
90 | 91 | ![스크린샷 2022-11-01 오후 10.54.59](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.54.59.png) 92 | 93 | 꺽은선 마크와 영역 마크의 조합입니다. `opacity`를 주어 꺽은 선이 보이도록 했습니다. 94 | 95 |
96 | 97 | ![스크린샷 2022-11-01 오후 10.56.34](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.56.34.png) 98 | 99 | 막대 마크와 직사각형 마크의 조합입니다. 마크의 높이와 너비를 설정할 수 있습니다. 100 | 101 |
102 | 103 | ![스크린샷 2022-11-01 오후 10.58.25](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2010.58.25.png) 104 | 105 | 이번엔 위 마크의 opacity를 주어 흐려지게 한 후, RuleMark를 추가했습니다. RuleMark는 연간 평균을 보여 준다는 것을 표시하고자 `annotation` modifier를 이용해서 주석을 추가했습니다. RuleMark 위 선행 정렬과 함께 텍스트 레이블이 추가됐습니다. 106 | 107 |
108 | 109 | ![스크린샷 2022-11-01 오후 11.01.28](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.01.28.png) 110 | 111 | 이렇게 마크를 조합하여 박스 플롯 차트, 멀티 시리즈 꺽은선 차트, 인구 피라미드 레인지 플롯 차트,스트림 그래프 멀티 시리즈 산점 차트 등 여러가지 차트를 그릴 수 있습니다. 112 | 113 | ## Plotting data with mark properties
마크 속성을 이용한 데이터 그리기 114 | 115 | ![스크린샷 2022-11-01 오후 11.05.20](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.05.20.png) 116 | 117 | SwiftUI Charts는 3가지 주요 데이터를 지원합니다. 양적, 명목적, 시간적 데이터입니다. 양적 데이터는 상품 판매량, 온도, 상품 가격 등의 수치입니다. 명목적 데이터 또는 범주형 데이터는 사람의 이름이나 대륙, 상품 유형 등과 같은 개별적 범주 혹은 그룹을 나타냅니다. 시간적 데이터는 특정 하루의 길이나 정확한 거래 시각 등과 같은 특정 시각이나 시간의 간격을 나타냅니다. 118 | 119 |
120 | 121 | ![스크린샷 2022-11-01 오후 11.09.33](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.09.33.png) 122 | 123 | 막대 마크의 동작은 X축과 Y축 프로퍼티로 표시된 데이터 유형에 따라 달라집니다. 막대의 방향은 양적 속성의 위치에 따라 달라집니다. 만약 X축에 판매량이 위치하면 막대 마크는 가로를 향한 모양이고 Y축에 판매량이 위치하면 막대 마크는 위 사진과 같이 세로를 향한 모양입니다. 124 | 125 |
126 | 127 | ![스크린샷 2022-11-02 오전 2.12.43](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.12.43.png) 128 | 129 | 마크 유형은 6가지이고 데이터의 유형은 3가지로 배열할 수 있는 조합은 여러가지 입니다. 덕분에 여러가지 기본 빌딩 블록으로 폭 넓은 차트 디자인을 지원합니다. 130 | 131 |
132 | 133 | ![스크린샷 2022-11-02 오전 2.15.39](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.15.39.png) 134 | 135 | Chart는 마크 프로퍼티를 이용해서 데이터를 그리면 매핑을 생성해서 추상적인 데이터를 적절한 속성값으로 변환합니다. 이 경우 판매량 값을 화면 공간의 Y 좌표로 변환합니다. 여기서 스케일이라는 용어가 사용되는데 스케일은 위치 속성에서 입력값을 적절한 화면 좌표로 변환할 때 몇가지 비율을 이용해 크기를 조절하기 때문에 붙여진 이름입니다. 마크로 데이터를 그릴 때 스케일이 생성되고 데이터를 해당 마크 속성으로 변환합니다. 136 | 137 |
138 | 139 | ![스크린샷 2022-11-02 오전 2.19.20](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.19.20.png) 140 | 141 | 예륻 들면, 위 차트에는 3개의 스케일이 있습니다. 각각의 요일을 X로, 판매량을 Y로, 도시를 foreground style로 변환합니다. 기본적으로 Swift Charts는 데이터에서 자동으로 스케일을 추정해서 즉시 차트를 완성합니다. 142 | 143 |
144 | 145 | ![스크린샷 2022-11-02 오전 2.24.42](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.24.42.png) 146 | 147 | 스케일을 직접 조절할 수 있습니다. 위 예시의 차트는 Y 스케일은 0~150으로 자동으로 선택됩니다. 이때, `chartYScale(domain:)` modifier를 이용해서 스케일의 영역을 0~200으로 고정시킬 수 있습니다. foreground style 또한 `chartForegroundStyleScale` modifier를 이용하여 색을 바꿀 수 있습니다. 148 | 149 | 150 | 151 | ## Custumizations
사용자 옵션 152 | 153 | ![스크린샷 2022-11-02 오전 2.27.39](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.27.39.png) 154 | 155 | 차트는 axes, legend, plot area로 구성됩니다. axes, legend는 차트를 해석할 수 있도록 돕고, plot area는 두 축 사이의 공간으로 데이터를 그리는 공간입니다. Swift Charts는 이 모든 요소를 커스텀(사용자 지정) 할 수 있습니다. 156 | 157 |
158 | 159 | ![스크린샷 2022-11-02 오전 2.31.18](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.31.18.png) 160 | 161 | 먼저 axes를 커스텀해보겠습니다. 위 코드는 X축의 레이블을 수정한 예시입니다. 각 분기의 첫 달인지 확인 한 후 첫달일 경우 강조하고 아니라면 눈금이나 레이블 없이 격자 선만 표시합니다. 이 X축은 분기 데이터를 보여주도록 커스텀했습니다. 162 | 163 |
164 | 165 | ![스크린샷 2022-11-02 오전 2.34.32](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.34.32.png) 166 | 167 | Y축 또한 커스텀할 수 있습니다. .extended 프리셋으로 UI와 어울리게 Y축을 설정할 수도 있고 168 | 169 |
170 | 171 | ![스크린샷 2022-11-02 오전 2.35.22](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.35.22.png) 172 | 173 | 축들을 숨김처리할 수도 있습니다. 174 | 175 |
176 | 177 | ![스크린샷 2022-11-02 오전 2.35.57](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.35.57.png) 178 | 179 | 자동 생성되는 Legend 또한 숨김처리할 수 있습니다. 180 | 181 |
182 | 183 | ![스크린샷 2022-11-02 오전 2.37.57](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.37.57.png) 184 | 185 | Plot Area를 커스텀해봅시다. `chartPlotStyle` modifier를 이용하고 후행 클로저에서는 기존 plot area를 받고 수정된 plot area를 반환하는 함수를 작성합니다. 위 예시처럼 차트의 카테고리 수에 따라 plotArea의 높이를 지정할 수 있습니다. 186 | 187 |
188 | 189 | ![스크린샷 2022-11-02 오전 2.41.02](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.41.02.png) 190 | 191 | 특별한 시각효과를 만들 수도 있습니다. 예를 들어 이 다크 모드 차트에서는 `.background` modifier로 분홍색 배경을 넣고 차트가 잘 보이도록 불투명도를 0.2로 설정한 뒤 분홍색으로 1포인트 두께 테두리를 들렀습니다. 192 | 193 |
194 | 195 | ![스크린샷 2022-11-02 오전 2.41.57](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.41.57.png) 196 | 197 | 앞에서 언급한 스케일은 X나 Y 같은 마크 프로퍼티에 데이터값을 매핑하는 함수입니다. Swift Charts는 `ChartProxy`를 이용해서 차트의 X와 Y 스케일에 접근할 수 있습니다. `ChartProxy`의 `position(for:)` 메소드로 주어진 데이터값의 위치를 얻을 수 있고 `value(at:)` 메소드를 사용해서 해당 위치의 데이터값을 얻을 수 있습니다. 이를 이용해서 차트와 다른 뷰를 조정할 수 있습니다. 198 | 199 |
200 | 201 | ![스크린샷 2022-11-02 오전 2.44.45](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.44.45.png) 202 | 203 | 예를 들어 인터랙티브 브러싱 뷰를 만들려고 합니다. 차트에서 드래그 제스처로 간격을 선택할 수 있고 그 간격은 세부 정보 뷰에서 행을 필터링하는 데 사용됩니다. `.chartOverlay`와 `chartBackground` modifier에서 `ChartProxy` 객체를 구성할 수 있습니다. 두 modifier는 SwiftUI의 오버레이, 배경 수정자와 유사하지만 `ChartProxy`를 제공합니다. 204 | 205 |
206 | 207 | ![스크린샷 2022-11-02 오전 2.49.31](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.49.31.png) 208 | 209 | 제스처가 시작한 위치에서 plot area의 원점을 빼고 제스터의 현재 위치에서 plot area의 원점을 빼는 방식과 `ChartProxy`를 이용해서 드래그 범위를 구할 수 있습니다. 210 | 211 |
212 | 213 | ![스크린샷 2022-11-02 오전 2.52.53](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.52.53.png) 214 | 215 | 범위를 구했다면 범위를 직사각형 마크로 정의하여 현재 선택한 날짜 범위를 시각화할 수도 있고 216 | 217 |
218 | 219 | ![스크린샷 2022-11-02 오전 2.53.33](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-11-02%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.53.33.png) 220 | 221 | 막대 사탕처럼 생긴 오버레이를 만들 수 있습니다. 222 | 223 |
224 | 225 | **End.** -------------------------------------------------------------------------------- /TIL/2022/TIL_2022:10:13_pinlayout_flexlayout.md: -------------------------------------------------------------------------------- 1 | Today I Learned 2 | 3 | ---- 4 | 5 | 2022.10.13 (목) 6 | 7 |
8 | 9 | - [PinLayout 리드미에서 오류 발견 pr](https://github.com/layoutBox/PinLayout/pull/254) 10 | 11 | ### PinLayout & FlexLayout 성능 비교 그래프 12 | 13 | PinLayout & FlexLayout은 오토레이아웃을 이용한 레이아웃 방식이 아닙니다. 따라서 자동으로 오토레이아웃을 잡아주는 UIStackView보다 8~12배 성능이 좋습니다. 이 프레임워크들은 메인스레드에서만 사용할 수 있는 뷰를 백그라운드 스레드로 끌고와 연산 처리를 하기 때문입니다. [제드님의 댓글](https://zeddios.tistory.com/1251)을 참고했습니다. 이번엔 이 프레임워크들을 어떻게 사용하는 지 공부해보겠습니다. 14 | 15 | 16 | 17 | ### PinLayout? 18 | 19 |
20 | 21 | - CSS의 절대 위치 방식을 차용해 만든 레이아웃 프레임워크 (오토레이아웃이 아님) 22 | - 오토레이아웃이 아니기 때문에 contianer의 크기가 바뀌거나 디바이스 회전모드가 바뀔 때 `UIView.layoutSubviews()`나 `UIViewController.viewDidLayoutSubviews()`에서 레이아웃을 업데이트해주어야합니다. (그래서 간단한 구성의 뷰에서 작성하나봐요.) 23 | - 코드 한 줄에, 하나의 View를 레이아웃해줍니다. 24 | 25 | ```swift 26 | label1.pin.topLeft().size(100) 27 | label2.pin.top().after(of: label1).size(100) 28 | label3.pin.left().below(of: label1).width(200).height(100) 29 | ``` 30 | 31 | - `pin`으로 시작하고 레이아웃을 구성하는 여러가지 메소드들을 제공합니다. (SnapKit의 `snp`와 비슷하게 사용됩니다.) 32 | 33 | ```swift 34 | let label1 = UILabel() 35 | label1.backgroundColor = .systemBlue 36 | self.view.addSubview(label1) 37 | let label2 = UILabel() 38 | label2.backgroundColor = .systemRed 39 | self.view.addSubview(label2) 40 | let label3 = UILabel() 41 | label3.backgroundColor = .systemMint 42 | self.view.addSubview(label3) 43 | ``` 44 | 45 | ### Edges Layout 46 | 47 | ```swift 48 | label1.pin.top(10).bottom(10).left(10).right(10) 49 | label1.pin.all(10) 50 | label1.pin.top(view.pin.safeArea.top) 51 | label1.pin.top(10%).bottom(10%).left(10%).right(10%) 52 | ``` 53 | 54 | top, bottom, left, right 하나하나 명시해서 사용 가능합니다., all로도 가능합니다, safeArea의 경우, `view.pin.safeArea`로 처리해야합니다. 파라미터에는 CGFloat, Percent, UIEdgeInsets 을 넣을 수 있습니다. CGFloat는 constraint와 마찬가지로 작동하고, Percent는 superview의 퍼센트를 계산해서 CGFloat로 변환해줍니다. 55 | 56 | ```swift 57 | top(_ offset: CGFloat) // 방법 1 58 | top(_ offset: Percent) // 방법 2 59 | top() // 방법 3 60 | top(_ margin: UIEdgeInsets) // 방법 4 61 | ``` 62 | 63 | `top`, `bottom`, `left`, `right`, `start`, `end`, `horizontally`, `vertically`는 모두 위의 방법으로 사용 가능합니다. 64 | 65 | ```swift 66 | all(_ margin: CGFloat) 67 | all() 68 | all(_ margin: UIEdgeInsets) 69 | ``` 70 | 71 | `all` 은 Percent 파라미터를 사용할 수 없습니다. 72 | 73 | ![pinlayout-anchors](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/pinlayout-anchors.png) 74 | 75 | 재밌는게 PinLayout에서는 topLeft, topRight와 같이 모서리에 뷰를 붙일 수 있는 메소드를 제공해줍니다. 76 | 77 | ```swift 78 | label1.pin.topRight().size(100) 79 | ``` 80 | 81 | 위와 같이 코드를 작성하면 아래와 같은 결과를 얻을 수 있습니다. 82 | 83 |
84 | 85 | ### Relative Edges Layout 86 | 87 | 상대적인 위치로 레이아웃을 잡을 수도 있습니다. `above(of: UIView)`, `above(of: [UIView])`와 같이 UIView, 또는 UIVIew배열 파라미터로 잡을 수 있습니다. 만약 배열로 넣을 경우 가장 위에 위치한 뷰의 위에 새로운 뷰를 위치시킵니다. (snapkit에는 없었던 거라 신기합니다!) `below` `before` `after` `left` `right` 메소드를 제공합니다. 88 | 89 | ```swift 90 | label1.pin.topLeft().size(100) 91 | label2.pin.top().after(of: label1).size(200) 92 | label3.pin.below(of: [label1, label2]).size(300) 93 | ``` 94 | 95 |
96 | 97 | 여러개의 뷰 레이아웃 관계를 이용해서 설정할 수도 있습니다. 두개의 뷰사이에 뷰를 위치시킬 수 있습니다. 98 | 99 | ```swift 100 | label1.pin.topLeft().size(100) 101 | label2.pin.topRight().size(100) 102 | label3.pin.after(of: label1).before(of: label2).height(100) 103 | ``` 104 | 105 |
106 | 107 | 뷰와 align을 맞출 수 있습니다. 만약 A뷰의 right에 위치시킨다고 할 때엔 VerticalAlignment 또한 설정할 수 있습니다. 108 | 109 | ```swift 110 | label1.pin.top(50).left().size(100) 111 | label2.pin.right(of: label1, aligned: .center).size(50) 112 | label3.pin.right(of: [label1,label2], aligned: .center).size(50) 113 | ``` 114 | 115 |
116 | 117 | ### Layout between other views 118 | 119 | 위와 다르게 아예 뷰사이에 위치시키는 메소드도 제공합니다. 120 | 121 | ```swift 122 | label1.pin.top(50).left().size(100) 123 | label2.pin.top(50).right().size(100) 124 | label3.pin.horizontallyBetween(label1, and: label2, aligned: .top).marginHorizontal(5).height(100) 125 | ``` 126 | 127 |
128 | 129 | 130 | 131 | 등등이 있고 이외에도 정말 많은 메소드들을 제공합니다. (레이아웃은 많이 잡아보면서 느는 것!) 132 | 133 | ### 💄 Style guide 134 | 135 | - `view.pin.[EDGE|ANCHOR|RELATIVE].[WIDTH|HEIGHT|SIZE].[pinEdges()].[MARGINS].[sizeToFit()]`순으로 작성 136 | 137 | ```swift 138 | view.pin.top().left(10%).margin(10, 12, 10, 12) 139 | view.pin.left().width(100%).pinEdges().marginHorizontal(12) 140 | view.pin.horizontally().margin(0, 12).sizeToFit(.width) 141 | view.pin.width(100).height(100%) 142 | ``` 143 | 144 | - TOP, BOTTOM, LEFT, RIGHT 순으로 작성 145 | - 레이아웃 코드가 너무 길면 개행 146 | 147 | ---- 148 | 149 |
150 | 151 | ### FlexLayout? 152 | 153 | - UIStackView를 개선한 레이아웃 프레임워크로 사용하기 간단하고 다양한 API를 제공합니다. 154 | - UIStackView보다 8~ 12배 빠름빠름! 155 | - PinLayout말고 FlexLayout을 쓰는 상황 156 | - 많은 뷰들의 레이아웃을 잡아야하는데 PinLayout의 세세한 컨트롤이 필요하지 않을 때 157 | - 복잡한 애니메이션이 없을 때 158 | - PinLayout과 같이 사용되는 레이아웃 관련 프레임워크 159 | - PinLayout을 FlexLayout container 안에서 사용가능하고 반대로도 사용가능합니다. (상황에 맞게 선택) 160 | 161 | ### Setup 162 | 163 | 먼저, container를 세팅합니다. 164 | 165 | ```swift 166 | class ExampleView: UIView { 167 | fileprivate let rootFlexContainer: UIView = UIView() 168 | 169 | init() { 170 | super.init(frame: .zero) 171 | rootFlexContainer.flex.define { flex in 172 | // 레이아웃 코드 173 | } 174 | } 175 | required init?(coder: NSCoder) { 176 | fatalError("init(coder:) has not been implemented") 177 | } 178 | } 179 | 180 | ``` 181 | 182 | 그리고 container의 레이아웃을 잡아줍니다. `layoutSubviews()` 또는 `viewDidLayoutSubviews()`에서 container의 레이아웃을 잡고 `flex.layout()`으을 실행합니다. 183 | 184 | ```swift 185 | override func layoutSubviews() { 186 | super.layoutSubviews() 187 | 188 | rootFlexContainer.pin.all(pin.safeArea) 189 | rootFlexContainer.flex.layout() 190 | } 191 | ``` 192 | 193 | 세팅이 완료됐습니다. 이제 레이아웃을 잡아주는 `flex.define()` 메소드에 대해서 알아보겠습니다. 194 | 195 | ### define() 196 | 197 | 레이아웃 잡는 코드를 구조화시킬 때 사용합니다. `flex`타입을 파라미터로 이용해서 하위 뷰(아이템)들을 추가합니다. 아래 1과 2는 같은 작업을 하는 코드입니다. 198 | 199 | ```swift 200 | // 1 201 | view.flex.addItem().define { (flex) in 202 | flex.addItem(imageView).grow(1) 203 | 204 | flex.addItem().direction(.row).define { (flex) in 205 | flex.addItem(titleLabel).grow(1) 206 | flex.addItem(priceLabel) 207 | } 208 | } 209 | 210 | // 2 211 | let columnContainer = UIView() 212 | columnContainer.flex.addItem(imageView).grow(1) 213 | view.flex.addItem(columnContainer) 214 | 215 | let rowContainer = UIView() 216 | rowContainer.flex.direction(.row) 217 | rowContainer.flex.addItem(titleLabel).grow(1) 218 | rowContainer.flex.addItem(priceLabel) 219 | columnContainer.flex.addItem(rowContainer) 220 | ``` 221 | 222 | 아래는 `Flex`타입이 가진 메소드들입니다. container에 아래 설정들을 할 수 있습니다. 223 | 224 | | FlexLayout Name | 225 | | --------------- | 226 | | **`direction`** | 227 | | **`wrap`** | 228 | | **`grow`** | 229 | | **`shrink`** | 230 | | **`basis`** | 231 | | **`start`** | 232 | | **`end`** | 233 | 234 | 하나씩 둘러보도록 하겠습니다. 235 | 236 | **direction**은 column, row를 설정할 수 있습니다. 디폴트값은 column입니다. 237 | 238 | ```swift 239 | rootFlexContainer.flex.direction(.column).define { flex in 240 | flex.addItem(view1).height(40) 241 | flex.addItem(view2).height(40) 242 | flex.addItem(view3).height(40) 243 | } 244 | ``` 245 | 246 |
247 | 248 | **wrap**은 flex container에 적용되는 메소드로, container 내의 아이템들이 여러줄일 때 다음 줄로 넘어갈지 아니면 한 줄에 보여줄 지 여부를 설정합니다. 디폴트는 nowrap으로 한 줄에 보여지도록 설정되어있습니다. 행을 축으로 하여 wrap설정을 하면 아래와 같이 나옵니다. 249 | 250 | ```swift 251 | rootFlexContainer.flex.direction(.row).wrap(.wrap).define { flex in 252 | flex.addItem(view1).width(50%).height(100) 253 | flex.addItem(view2).width(50%).height(100) 254 | flex.addItem(view3).width(50%).height(100) 255 | } 256 | ``` 257 | 258 |
259 | 260 | **grow**는 flex item에 적용되는 것으로, 아이템이 더 커질 수 있을지에 대한 여부를 설정합니다. 디폴트는 0으로, 0이면 아이템은 커지지 않고 0이 아닌 0초과의 값들은 남는 공간을 모두 채워버립니다. (`grow`가 0인 아이템과 같이 쓰인 경우에) `grow`가 1인 아이템이 있다면 상대적 비율대로 다른 아이템보다 커집니다. 즉, 아이템이 모두 grow(1)로 설정되어있다면 아이템의 사이즈는 모두 같아집니다. 아이템1과 아이템2의 `grow`가 1과 2로 설정되어있다면 아이템2는 아이템 1보다 2배 커야합니다. **shrink**는 `grow`의 반대로 얼마나 더 줄어들 수 있는 지에 대한 여부를 설정합니다. 261 | 262 | > grow와 shrink는 텍스트로 설명하기에 굉장히 어렵습니다. 직접 작성해보아야합니다. 263 | 264 | ```swift 265 | rootFlexContainer.flex.wrap(.wrap).define { flex in 266 | flex.addItem(view1).grow(1) 267 | flex.addItem(view2).grow(1) 268 | flex.addItem(view3).grow(2) 269 | } 270 | ``` 271 | 272 |
273 | 274 | 275 | 276 | **basis**는 flex item에 적용되는 것으로, 너비와 높이 값입니다. grow와 shrink에 의해서 남는 nil로 설정하면 `auto`라는 의미로 아이템의 크기와 같아집니다. 만약 아이템의 크기가 정해져있지 않다면, 아이템의 콘텐츠에 따라 정해집니다. 277 | 278 | ```swift 279 | basis(_ : CGFloat?) 280 | basis(_ : FPercent) 281 | ``` 282 | 283 | **justifyContent**는 flex conainer에 적용되는 것으로, `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly` 의 값으로 아이템의 정렬을 설정합니다. 디폴트값은 `start`입니다. 아이템들이 어떻게 위치하는 지에 대해선 [이 링크](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-10-13%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%205.32.00.png)를 확인하면 됩니다:) 284 | 285 | ```swift 286 | rootFlexContainer.flex.justifyContent(.spaceBetween).define { flex in 287 | flex.addItem(view1).height(100) 288 | flex.addItem(view2).height(100) 289 | flex.addItem(view3).height(100) 290 | } 291 | ``` 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 |
300 | 301 | . 302 | 303 | . 304 | 305 | . 306 | 307 | . 308 | 309 | . 310 | 311 | 또 다른 여러가지 메소드들이 있는데 사용해보면서 하나씩 알아보도록 하겠습니다. 추가로 알면 편한 것들은 `margin`, `padding`을 추가하거나 `backgroundColor`를 설정할 수 있습니다. 312 | 313 | 314 | 315 | ### addItem() 316 | 317 | 메소드들에 대해서 알아봤는데 아이템을 어떻게 추가할까요? (위에서 사용하긴 했지만..) 318 | 319 | **addItem(:UIView)**을 이용하여 원하는 View를 추가할 수 있습니다. 하지만 항상 rootContainer 안에 새로운 뷰를 추가해야합니다. 320 | 321 | ```swift 322 | rootFlexContainer.flex.addItem(view1).height(100) 323 | rootFlexContainer.flex.addItem(view2).height(100) 324 | rootFlexContainer.flex.addItem(view3).height(100) 325 | ``` 326 | 327 | **addItem()**을 이용하여 기본 UIView를 추가할 수 있습니다. 그냥 간단한 뷰를 만들 때 유용할 것 같습니다. (구분선) 328 | 329 | ```swift 330 | rootFlexContainer.flex.addItem().height(100).backgroundColor(.gray) 331 | rootFlexContainer.flex.addItem().height(100).backgroundColor(.green) 332 | rootFlexContainer.flex.addItem().height(100).backgroundColor(.black) 333 | ``` 334 | 335 | Flex 아이템의 UIView 컴포넌트에 접근하기 위해서는 두가지 방법이 있습니다. `flex.view`로 접근하는 방식과 선언 후 Flex 아이템에 추가하는 방식입니다. 336 | 337 | ```swift 338 | view.flex.direction(.row).padding(20).alignItems(.center).define { (flex) in 339 | flex.addItem().width(50).height(50).define { (flex) in 340 | flex.view?.alpha = 0.8 341 | } 342 | } 343 | 344 | view.flex.direction(.row).padding(20).alignItems(.center).define { (flex) in 345 | let container = UIView() 346 | container.alpha = 0.8 347 | 348 | flex.addItem(container).width(50).height(50) 349 | } 350 | ``` 351 | 352 | ### layout() 353 | 354 | container에 속한 아이템들의 레이아웃을 잡아주는 메소드입니다. 항상 불러주어야하는 메소드로, `fitContainer`, `adjustWidth`, `adjustHeight` 를 값으로 가지고 있습니다. 355 | 356 | `fitContainer`는 디퐅트값으로 하위 아이템들이 container 크기 안에서 레이아웃이 잡힙니다. `adjustWidth`는 하위 아이템들이 container의 너비에 맞게 레이아웃이 잡힙니다. container의 높이는 하위 아이템의 크기에 맞게 조절됩니다. `adjustHeight`는 하위 아이템들이 container의 높이에 맞게 레이아웃이 잡히고너비는 아이템들의 크기에 맞게 조절됩니다. 357 | 358 | ```swift 359 | rootFlexContainer.pin.all(pin.safeArea) 360 | rootFlexContainer.flex.layout(mode: .adjustHeight) 361 | ``` 362 | 363 | | fitContainer | adjustHeight | 364 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 365 | | | | 366 | 367 | 368 | 369 | ### ⚠️ 주의할 사항 370 | 371 | SPM으로 FlexLayout 설치하고 빌드할 때, TARGET -> Build Settings -> Apple Clang-Preprocessing -> Preprocessor Macros에서 `FLEXLAYOUT_SWIFT_PACKAGE=1` 추가 (리드미에 적혀있습니다..😓) 372 | 373 | ![image-20221013174908276](https://raw.githubusercontent.com/hello-woody/img-uploader/master/uPic/image-20221013174908276.png) 374 | 375 | 376 | 377 | - https://github.com/layoutBox/PinLayout 378 | - https://devscope.io/code/layoutBox/FlexLayout/issues/200 379 | - https://github.com/layoutBox/FlexLayout#performance 380 | - https://zeddios.tistory.com/1251 --------------------------------------------------------------------------------