ããã«ã¡ã¯ãohayoukenchanã§ãã
ä»åã¯SwiftUIã«ã¤ãã¦ã§ãã ãããªã§ã¯iOS13ããµãã¼ããã¦ããã®ã§ãä¸é¨iOS13ããµãã¼ãããå 容ãå«ã¾ãã¾ãã
ã·ã¹ãã ãé·æã¡ãããå
çªç¶ã§ãããã³ãããã®ã¨ã³ã¸ãã¢ãªã³ã°çµç¹ã¯Tech Visionã¨ãããã®ãæ²ãã¦ãããæ¦è¦ã¨ãã¦ã¯ãã¿ããªã§ã¨ã³ã¸ãã¢çµç¹å¼·ããã¦ããããããçãªãã¨ãæ¸ãã¦ãããã§ããããã®ãªãã®ï¼ã¤ã®æè¡åã¨ãã¦ãã·ã¹ãã ãé·æã¡ãããåããéè¦ãªæè¡åã¨ãã¦æ¨é²ãã¦ãã¾ãã
ãããªiOSã¢ããªã§ãææ°æè¡ã®æ©æµãåãç¶ããããããæ¥ã ã³ã¼ãã®ã¢ãããã¼ããè¡ã£ã¦ããã¾ãã
å æ¥ãå¼ç¤¾ã§ã¯Tech Visionæ¨é²ã®ä¸ç°ã§ãæè¡ç®æ¨ãã«ã·ã§ãªããã®ãéå¬ããã¾ããã詳細ã¯ãã¡ãã®ãã¹ãããã²è¦ãã¦ã¿ã¦ãã ããï¼
æè¡ç®æ¨ãã«ã·ã§ã¯ç¤¾å ã¤ãã³ãã§ãåèªæ¯è¼çèªç±ã«æ°ã«ãªãæè¡ãé¸ãã§çºè¡¨ããã®ã§ãããèªåã¯CoreMLã¨Visionã使ã£ã¦ç»ååé¡ããããã¨ãã¸æ½åºããç»åãSKTextureã«ãã¦SpriteKitã§éã¶ã¨ããå 容ãçºè¡¨ãã¾ããã
iOSçã®ãããªããç´è¿ã¾ã§ã¯StoryboardãUIViewã使ã£ãéçºããã¦ãã¾ããã
Storyboardã使ã£ãéçºã®å ´åãUIã®åºåºã¨ãªãStoryboardã§ã¯å®è£ å 容ã¯ãããã¾ãããããããUIKitã使ã£ã¦å®è£ ãä»ãå ãã¦ããã®ã§ããUIãçµã¿ç«ã¦ãã®ã«ãUITableViewCellãUIViewãç¶æ¿ãããã¡ã¤ã«ãå¢ããã¦ãããã¨ã«ãªãã¾ãã
ãªã«ãè¡ããããããããªãstoryboardã®ä¾
Storyboardã使ã£ãéçºãã¬ã¬ã·ã¼ã¨ã¯è¨ãåãã¾ããããæ¨ä»ãreactãçé ã«ã宣è¨çUIã§æ¸ãããã³ã¼ãã®è¦éãã®è¯ããéã«UIKit(storyboard)ã§UIãçµã¿ç«ã¦ã¦ããã³ã¼ãã®è¦éãã®æªããèããã¨ããµãã¼ããã¼ã¸ã§ã³ãèæ ®ãã¤ã¤ãããããéçºããã·ã¹ãã ã«é¢ãã¦ã¯SwiftUIã使ã£ã¦éçºãã¦ãããã¨ããçµè«ã«ãªãã¾ããã
ã¢ã¼ããã¯ãã£ã«ã¤ãã¦
SwiftUIã¨ç¸æ§ã®è¯ãã©ã¤ãã©ãªã«TCA(The Composable Architecture)ãããã¾ããç¶æ ã®éä¸ç®¡çããããscopeã使ããã¨ã§watchããstateã®ç¯å²ãéå®ã§ãããã¨ã§ãç¡é§ã«åæç»ãçºçããªãã£ããããã¹ãã©ã¤ãã©ãªãç¨æããã¦ããã®ã§é常ã«é åçã§ããããTCAã®æ¸å¿µã¨ãã¦ã¯ãViewãå«ãã¦TCAã«å¼·ãä¾åãã¦ãã¾ãã®ã§ãTCAã使ããªããªã£ãå ´åã«å¼ãã¯ããã®ã大å¤ããã§ãããã¨ãçç±ã§TCAã®æ¡ç¨ã¯è¦éã£ã¦ãã¾ãã
ãã¾ã¾ã§ã®iOSéçºã®ã©ã¤ãã©ãªã®æµè¡ãå»ããèæ ®ããã¨ä»ã«ãããã®ãã§ã¦ãã¦å»ããå¯è½æ§ãããã¨é«ããã¨ããè°è«ããã¾ããã
ä½è«ã§ããFluxãã¼ã¹ã®ã©ã¤ãã©ãªã®æåã©ããã«reactã®reduxãããã¨æãã®ã§ãããreactãhooksãå°å ¥ãããã¨ã§reduxãªãã§ç¶æ 管çã§ãããããªã¢ããã¼ããã¨ã£ã¦ãã¦ããã®ã§ç¶æ 管çãã©ã®å ´æã§è¡ã£ã¦ããã®ãä»å¾ãæ°ã«ãªã£ã¦ãã¾ãã
https://github.com/pointfreeco/swift-composable-architecture
ãããªiOSçã®ãªã¢ã¯ãã£ãããã°ã©ãã³ã°æ§æ
ãããªiOSçã¯ãMVVMã¢ã¼ããã¯ãã£ã§æ§æããã¦ãããAPIãUIããã®ã¤ãã³ãéä¿¡ãªã©ã«RxSwiftãRxCocoaã使ç¨ãã¦ãã¾ããSwiftUIãå°å ¥ããã«ããããRxSwiftãåãé¢ãã代ããã«Combineãå°å ¥ãããã¨ãæ¤è¨ãã¾ããããRxSwiftã¸ã®ä¾åãå¼·ããã¨ã¨ãRxã³ãã¥ããã£ã¯æ´»çºã§ã©ã¤ãã©ãªæ´æ°ãç©æ¥µçã«è¡ããã¦ãããã¨ãããç¡çã«å¼ãé¢ããããªé¸æã¯ãã¦ãã¾ããã
æ°è¦ã§UIãä½æããå ´åãç¶æ³ã«å¿ãã¦RxSwiftã§æµãã¦ããå¤ãCombimeã®Publisherã«ããããããã¦ãã¾ããä¸ã¤ã®ãã¡ã¤ã«ã«Cancellable
ã¨DisposeBag
両æ¹æ¸ããªãã¦ã¯ãããªããªã©ãã³ã¼ãã®è¦éããè¥å¹²æªããªãã®ã§ãããããã¯ç§»è¡æã¨ããæãæ¹ãè¿ãã¨ããã£ã¦ãã¦ãç¶ç¶çã«éç¨ãç¶ãã¦ãããã¨ãè¦éã«èããã¨ããã®æ©è½èªä½ãªããªããããããªããã該å½æ©è½ã«å¤§å¹
ãªã¢ãããã¼ãããããããããã¾ãããå¯è½æ§ãèæ
®ããã¨ããããªãã®ã§ä»ã¯ç§»è¡æã¨ãã¦ãã®ãããªä»çµã¿ã«ãªã£ã¦ãã¾ãã
fileprivate let disposeBag = DisposeBag() fileprivate var cancellables: [AnyCancellable] = []
Hosting Controllerã®åãæ±ã
ãããªã§ã¯æ¢åã®ã¢ã¼ããã¯ãã£ã¨ã®å
¼ãåãããããç»é¢é·ç§»ã¯ UIViewControler
ã«ä»»ãããã¨ã«ãã¾ãããUIHostingController
ãç¶æ¿ããã¯ã©ã¹ã® rootView
ã« SwiftUIã® View
ã渡ãããã«ãã¦ãã¾ãã super.init(rootView:)
ããã¨ãã«classå
ã®ããããã£ãåæåãã¦æ¸¡ãã¦ãããããã©Superã¯ã©ã¹ã®åæåãçµãã£ã¦ãªãã®ã«ãµãã¯ã©ã¹ã®ããããã£ã«ã¢ã¯ã»ã¹ãããªã¨æããã¦ãã¾ãã¾ãã
ã³ã³ãã¤ã«ã¨ã©ã¼ã®ä¾
class DiagnosisInterestingTopicsViewController: UIHostingController< DiagnosisInterestingTopicSelectView > { private var cancellables: [AnyCancellable] = [] var viewModel: DiagnosisInterestingTopicsViewModel() init(interstingTopics: InterestingTopicsResponse) { super .init( rootView: DiagnosisInterestingTopicSelectView( viewModel: viewModelã// 'self' used in property access 'viewModel' before 'super.init' call ) ) } ã»ã»ã»
ãã®å ´åãsuper.init(rootView:)
ã®åã«ViewModelãä½ã£ã¦ããã¨ã³ã³ãã¤ã«ã¨ã©ã¼ãåé¿ãããã¨ãåºæ¥ã¾ããrootViewã«æå®ãããViewã®å¼æ°ã¨Classå
é¨ã§åãæ±ãviewModelãä¸è´ãããããã«ãããã¦ã¾ãããè¦éãã¯æªãã§ããã
ã³ã³ãã¤ã«ãæåããä¾
class DiagnosisInterestingTopicsViewController: UIHostingController< DiagnosisInterestingTopicSelectView > { private var cancellables: [AnyCancellable] = [] var viewModel: DiagnosisInterestingTopicsViewModel! init(interstingTopics: InterestingTopicsResponse) { let viewModel = DiagnosisInterestingTopicsViewModel( interstingTopics: interstingTopics ) super .init( rootView: DiagnosisInterestingTopicSelectView( viewModel: viewModel ) ) self.viewModel = viewModel } ã»ã»ã»
HostingControllerã§æ¢åUIKitã®ç»é¢ã表示ãã
éä¿¡ä¸ç»é¢ã¯SVProgressHUD
ã使ç¨ãã¦ãã¾ããSwiftUIã使ã£ãç»é¢ã§ãæ¢åã®UIã使ç¨ãããã®ã§ãSwiftUIå´ã§SVProgressHUD
ã表示ããã¨ãSwiftUIã®æç»é åãããªã¼ãã¼ã¬ã¤ããããNavigationBarãªã©ããªã¼ãã¼ã¬ã¤ã®ä¸ã«è¡¨ç¤ºããã¦ãã¾ãã¾ããããã®ãããSVProgressHUD
ã¯UIHostingController
ããå¼ã¶ããã«ãã¾ããã
ãããªiOSçã¯iOS13ããµãã¼ããã¦ãããããiOS13ã§æ¤è¨¼ããã¨ããéä¿¡ãçºçãã¦ããªã¼ãã¼ã¬ã¤ã表示ããããæ¤è¨¼ããã¨ããviewDidLoad
ã§å¦çãã¦ãviewModel.$progressState
ã«å¤ãæµãããviewDidAppear
ã§å¼ã¶ãã¨ã§åé¿ã§ãã¾ãããåå ã¯åãã£ã¦ãªãã§ãã
class DiagnosisRegionSelectViewController: UIHostingController<DiagnosisRegionSelectContainerView>, DiagnosisPageable { ... åæåå¦çãªã©çç¥ override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // iOS13ã ã¨viewDidLoadã«ããã¨å¼ã°ããªãã®ã§viewDidAppearã§å¦ç bindUI() } func bindUI() { viewModel.$progressState .receive(on: DispatchQueue.main) .sink { state in self.showProgressView(state) } .store(in: &cancellables) } func showProgressView(_ state: ProgressState) { switch state { case .asleep: SVProgressHUD.dismiss() case .connecting: SVProgressHUD.show(withStatus: "éä¿¡ä¸", maskType: .black) } }
SwiftUIã§å®£è¨çã«ãããè¯ã
SwiftUIå°å ¥ã®ã¡ãªããã§ãã宣è¨çUIãå®ç¾ããããã®ã§ãè¤éãªãã¸ãã¯ã¯æãããUIã®çµã¿ç«ã¦ã«éä¸ããã¦ãã¾ãããã¡ãã¯SwiftUIã§æ¸ããæ©è½ã§ããï¼ç»é¢ãæ§æããã®ã«50è¡ãããã®SwiftUIãã¡ã¤ã«ãæ¸ãã ãã ã£ãã®ã§å¤§å¤è¦éããããï¼storyboardãcellããããªããªãã¦ï¼ï¼
æåãã¾ãã
struct DiagnosisRegionSelectSearchView: View { @ObservedObject var viewModel: DiagnosisRegionSelectViewModel private let maxCharacterLength = 7 var body: some View { VStack(spacing: 0) { SearchBarRepresentable( text: $viewModel.zipCode, maxCharacterLength: maxCharacterLength, placeholder: "éµä¾¿çªå·ãå ¥åãã", keyboardType: .numberPad ) .onReceive( viewModel.$zipCode.dropFirst(), perform: { zipCode in if maxCharacterLength == zipCode.count { viewModel.apply( .onSearchZipCode(zipCode) ) self.closeKeyboard() } else { // ãªã«ãããªã } } ) ... ä¸é¨çç¥ if viewModel.cities.isEmpty { Text("å ¥åããéµä¾¿çªå·ã¯åå¨ãã¾ããã§ããã\nååº¦å ¥åãã試ããã ãã") .font(.system(size: 11)) .foregroundColor(Color("Error")) .multilineTextAlignment(.center) .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 24) } else { VStack(alignment: .leading, spacing: 0) { ForEach(Array(viewModel.cities.enumerated()), id: \.offset) { index, city in Text("\(city.prefectureName) \(city.cityName1) \(city.cityName2)") .font(.system(size: 12)) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(RoundedRectangle(cornerRadius: 20)) .onTapGesture { viewModel.apply(.onChangeViewStateTapped(.confirm(city: city))) } if index < viewModel.cities.count - 1 { Divider() .padding(.leading, 15) } else { Divider() } } } } Spacer() } .padding(.top, statusBarHeight()) } }
ã¾ããViewModelã¨UIã§åä¸æ¹åã®ãã¤ã³ãã£ã³ã°ãå®ç¾ãããã®ã§ãViewModelã¯å¤ããå ¥åå¤ãåãåããã¨ãã§ããããã«ä»¥ä¸ã®protocolã«æºæ ããã¦ããã¾ãã
protocol UnidirectionalDataFlowType { associatedtype InputType func apply(_ input: InputType) }
final class DiagnosisRegionSelectViewModel: UnidirectionalDataFlowType { typealias InputType = Input private var cancellables: [AnyCancellable] = [] private let disposeBag = DisposeBag() // Combine private let onSearchZipCodeSubject = PassthroughSubject<String, Never>() // MARK: Input enum Input { case onSearchZipCode(String)[f:id:ohayoukenchan:20220329104948p:plain][f:id:ohayoukenchan:20220329104948p:plain] ... ç¥ } func apply(_ input: Input) { switch input { case .onSearchZipCode(let zipCode): onSearchZipCodeSubject.send(zipCode) ... ç¥ } } ... ç¥
ãããããã¨ã§viewModelã®å¤ãã viewModel.apply(.onSearchZipCode(zipCode))
ã®ããã«viewModelã¸å¤ãæµããã¨ãã§ãã¾ããswiftUIã® .onTapGesture
ã«è¤éãªå¦çãæ¸ããªããã¨ã§ã³ã¼ãã®è¦éããè¯ããªã£ã¦ããã¨æãã¦ãã¾ãã
æå¾ã«
iOS13ã ã¨GeometryReaderããã¾ãåæåã§ããªãã£ããã.ignoresSafeArea(.keyboard, edges: .bottom)
ãé対å¿ãªã®ã§keyboardãéããã¨ãã«ç»é¢ãæ¼ãä¸ããå¦çãããããèªåã§ç¨æããªãã¨ãããªãã£ãã NSTextAttachment
ã®è²ãå¤ãããªããªã©ãæ¯æ½çå¿
ãã¨ãã£ã¦ããã»ã©iOS13ã¸ã®å¯¾å¿ãè¡ã£ã¦ãã¾ããã追å ã§iOS13åãã®å¯¾å¿ãããªããã°ãããªããã¨ãèããã¨ããµãã¼ããã¼ã¸ã§ã³ãiOS14以éã«ãªã£ã¦ããSwiftUIãå°å
¥ããã»ããç¡é£ããªã¨ããå°è±¡ã§ãã
è¿ããã¡ã«å¼ç¤¾ã¢ããªããµãã¼ããã¼ã¸ã§ã³ã®è¦ç´ããè¡ããiOS14以éã§ãµãã¼ãããã¦ãã StateObject ã LazyVGrid
ãªã©ã使ããããã«ãªããã¾ãã¾ãéçºã楽ãããªã£ã¦ãããã§ããä»å¾ãå¼ãç¶ãã·ã¹ãã ãé·æã¡ãããåãé¤ã£ã¦ããããã