ãã®è¨äºã¯ ãã¬ã¿ Advent Calendar 2019 ã®23æ¥ç®ã®è¨äºã§ãã
ããã«ã¡ã¯ãiOS & Androidã¨ã³ã¸ãã¢ã®å±±å£ã§ãã ãã¬ã¿ã§ã¯ã主ã«ãã¬ã¿nowã®éçºãè¡ã£ã¦ãã¾ãã
ããã¾ã§ãUIKitãRxSwift(RxCocoa)ã使ã£ãMVVMã¢ã¼ããã¯ãã£ã§å®è£ ãããã¨ãå¤ãã£ããã¨ãããã SwiftUIãCombineã使ã£ãå ´åã«ã¯ã©ãæ¸ãã°ãããèãã¦ã¿ã¾ããã
ãªãããã®è¨äºã¯SwiftUIã使ãéã«éè¦ãªãã¤ã³ãã¨ãªã PropertyWrappersãDynamicMemberLookupãããããå©ç¨ããObservedObjectãBindingã«ã¤ã㦠ã®ç¥èãããåæã§è¨è¿°ãã¦ãã¾ãã
ViewModel Protocol ã®å®ç¾©
æ©éã§ãããã³ã¼ããã説æãã¦ããã¾ãã ã¾ãããã¹ã¦ã®ViewModelãæºæ ãã¹ãã·ã³ãã«ãªProtocolãå®ç¾©ãã¾ããã æå³ã¨ãã¦ã¯ä¸è¨2ç¹ãæãããã¾ãã
- ãã¼ã éçºããä¸ã§ã®ViewModelã®æ¸ãæ¹ãããç¨åº¦çµ±ä¸ããã
- SwiftUIå´ã§å©ç¨ããããå½¢ã«ããã
/// ViewModelãæºæ ãããããã³ã« protocol ViewModelObject: ObservableObject { associatedtype Input: InputObject associatedtype Binding: BindingObject associatedtype Output: OutputObject var input: Input { get } var binding: Binding { get } var output: Output { get } } extension ViewModelObject where Binding.ObjectWillChangePublisher == ObservableObjectPublisher, Output.ObjectWillChangePublisher == ObservableObjectPublisher { var objectWillChange: AnyPublisher<Void, Never> { return Publishers.Merge(binding.objectWillChange, output.objectWillChange).eraseToAnyPublisher() } } protocol InputObject: AnyObject { } protocol BindingObject: ObservableObject { } protocol OutputObject: ObservableObject { } @propertyWrapper struct BindableObject<T: BindingObject> { @dynamicMemberLookup struct Wrapper { fileprivate let binding: T subscript<Value>(dynamicMember keyPath: ReferenceWritableKeyPath<T, Value>) -> Binding<Value> { return .init( get: { self.binding[keyPath: keyPath] }, set: { self.binding[keyPath: keyPath] = $0 } ) } } var wrappedValue: T var projectedValue: Wrapper { return Wrapper(binding: wrappedValue) } }
ViewModelObject
- å¾è¿°ããInputObjectãBindingObjectãOutputObjectãããããã£ã¨ãã¦æã¤
- å¤ã®å¤æ´ãSwiftUIã«éç¥ããããã«
ObservableObject
ã«æºæ
InputObject
- ã¦ã¼ã¶ããã®å
¥åã§ããã¿ãããªã©ã®ã¤ãã³ãä¸è¦§ã
PassthroughSubject
ã¨ãã¦å®ç¾©
BindingObject
TextField
ã®æååãToggle
ã®ãªã³ã»ãªããªã©ã®ãããªåæ¹åãã¤ã³ãã£ã³ã°ããå¤ã®ä¸è¦§ã@Published
ã¨ãã¦å®ç¾©- å¤ã®å¤æ´ãSwiftUIã«éç¥ããããã«
ObservableObject
ã«æºæ
OutputObject
Text
ã«è¡¨ç¤ºããæååãList
ã«è¡¨ç¤ºããé åãªã©UIã«åºåããå¤ã®ä¸è¦§ã@Published
ã¨ãã¦å®ç¾©- å¤ã®å¤æ´ãSwiftUIã«éç¥ããããã«
ObservableObject
ã«æºæ
SwiftUIããViewModelã使ãããã®å·¥å¤«
SwiftUIå´ã¯ViewModelã¤ã³ã¹ã¿ã³ã¹ã@ObservedObject
ã¨ãã¦ä¿æãã¾ãã
ViewModelãæã¤BindingObject
ã¨OutputObject
ã®å¤ãæ´æ°ãããéã«åæç»ããããã®ã§ã
ãã®2ã¤ã®objectWillChange
ããã¼ã¸ããçµæãViewModelObject
ã®objectWillChange
ã¨ãã¦ãã¾ãã
ã¾ããBindingObject
ããåæ¹åãã¤ã³ãã£ã³ã°ããããã®Binding
ãåãåºãããã«ã
@propertyWrapper
ã¨@dynamicMemberLookup
ãå©ç¨ãã¦ãã¾ãã
MVVMã®å®è£
ã§ã¯å®éã«ViewModelObject
ãå©ç¨ãã¦ããµã¤ã³ã¢ããç»é¢ã½ããã®ãå®è£
ãã¦ã¿ã¾ãã
UIã®æ§æè¦ç´ ã¯ä¸è¨ã®éãã§ãã
- IDå ¥åæ¬ï¼6æå以ä¸ï¼
- ãã¹ã¯ã¼ãå ¥åæ¬ï¼8æå以ä¸ï¼
- 確èªç¨ãã¹ã¯ã¼ãå ¥åæ¬
- è¦ç´åæã®ãã°ã«ã¹ã¤ãã
- ãµã¤ã³ã¢ãããã¿ã³
ViewModelã®å®è£
final class SignUpViewModel: ViewModelObject { final class Input: InputObject { /// ãµã¤ã³ã¤ã³ãã¿ã³ã®ã¿ããã¤ãã³ã let signUpTapped = PassthroughSubject<Void, Never>() } final class Binding: BindingObject { /// ã¦ã¼ã¶ID @Published var id = "" /// ãã¹ã¯ã¼ã @Published var password = "" /// 確èªç¨ãã¹ã¯ã¼ã @Published var confirmPassword = "" /// å©ç¨è¦ç´åæãã©ã° @Published var agreed = false /// ãµã¤ã³ã¢ããå®äºã¢ã©ã¼ã表示ãã©ã° @Published var isCompletionAlertPresented = false } final class Output: OutputObject { /// ãµã¤ã³ã¢ãããã¿ã³ã®æå¹ãã©ã° @Published fileprivate(set) var isSignUpEnabled = false } let input: Input @BindableObject private(set) var binding: Binding let output: Output private var cancellables = Set<AnyCancellable>() init() { let input = Input() let binding = Binding() let output = Output() /// ã¦ã¼ã¶IDã®ããªãã¼ã·ã§ã³ï¼6æå以ä¸ï¼ let isIdValid = binding.$id .map { $0.count >= 6 } /// ãã¹ã¯ã¼ãã®ããªãã¼ã·ã§ã³ï¼6æå以ä¸ï¼ let isPasswordValid = binding.$password .map { $0.count >= 8 } /// 確èªç¨ãã¹ã¯ã¼ãã®ããªãã¼ã·ã§ã³ï¼`password`ã¨ä¸è´ï¼ let isConfirmPasswordValid = Publishers .CombineLatest( binding.$password, binding.$confirmPassword ) .map { $0.0 == $0.1 } /// ãµã¤ã³ã¢ãããã¿ã³æå¹ãã©ã° /// - ãã¹ã¦ã®å ¥åå 容ãæå¹ /// - å©ç¨è¦ç´ã«åæ let isSignUpEnabled = Publishers .CombineLatest4( isIdValid, isPasswordValid, isConfirmPasswordValid, binding.$agreed ) .map { $0.0 && $0.1 && $0.2 && $0.3 } /// ãµã¤ã³ã¢ããå®äºã¢ã©ã¼ãã®è¡¨ç¤º let isCompletionAlertPresented = input.signUpTapped .flatMap { // å®éã«ã¯Model層ã®ãµã¤ã³ã¢ããå¦çãå¼ã³åºã Just(true) } // çµã¿ç«ã¦ãã¹ããªã¼ã ãbinding, outputã«åæ cancellables.formUnion([ isCompletionAlertPresented.assign(to: \.isCompletionAlertPresented, on: binding), isSignUpEnabled.assign(to: \.isSignUpEnabled, on: output) ]) self.input = input self.binding = binding self.output = output } }
ViewModelã®å®è£
ã¯åºæ¬çã«ã¤ãã·ã£ã©ã¤ã¶ã®ã¿ã§ãInputObject
ãBindingObject
ããæµãã¦ããã¹ããªã¼ã ã®åæãè¡ãã
ãã®çµæãBindingObject
ãOutputObject
ã«åæ ããã¦ãã¾ããBindingObject
ã«å®ç¾©ããã¦ããåæ¹åãã¤ã³ãã£ã³ã°ç¨ã®å¤ã¯@Published
ã¨ãã¦å®ç¾©ããã¦ããããã
$
ãã¤ãããã¨ã§Publisher
ãåãåºããã¨ãã§ãã¾ãã
Viewã®å®è£
import Combine import SwiftUI struct SignUpView: View { @ObservedObject var viewModel: SignUpViewModel var body: some View { VStack(alignment: .leading, spacing: 16) { TextField("ã¦ã¼ã¶ID", text: viewModel.$binding.id) .textFieldStyle(RoundedBorderTextFieldStyle()) TextField("ãã¹ã¯ã¼ã", text: viewModel.$binding.password) .textFieldStyle(RoundedBorderTextFieldStyle()) TextField("ãã¹ã¯ã¼ãï¼ç¢ºèªç¨ï¼", text: viewModel.$binding.confirmPassword) .textFieldStyle(RoundedBorderTextFieldStyle()) Toggle(isOn: viewModel.$binding.agreed) { Text("å©ç¨è¦ç´ã«åæãã") } Color.clear.frame(height: 16) Button(action: { self.viewModel.input.signUpTapped.send(()) }) { HStack { Spacer() Text("ãµã¤ã³ã¢ãã") .padding(.vertical) Spacer() } } .disabled(!viewModel.output.isSignUpEnabled) Spacer() } .padding(.horizontal) .padding([.top], 64) .alert(isPresented: viewModel.$binding.isCompletionAlertPresented) { Alert(title: Text("ãµã¤ã³ã¢ããå®äº"), dismissButton: .cancel(Text("OK"))) } } }
@ObservedObject
ã¨ãã¦ViewModelã®ããããã£ãå®ç¾©ãã¾ãã
ãã¿ã³ã¿ãããªã©ã®ã¢ã¯ã·ã§ã³ãåãåãã¯ãã¼ã¸ã£ã§ãInputObject
ã®Subject
ã«å¯¾ãã¦ã¤ãã³ããéä¿¡ãã¦ãã¾ãã
TextFieldãªã©ã®åæ¹åãã¤ã³ãã£ã³ã°ããå¤ã«ã¤ãã¦ã¯ãviewModel.$binding
ã¨æ¸ããã¨ã§BindableObject.Wrapper
ãåãåºããã¨ãã§ãã
DynamicMemberLookupã«ããBinding
ããããã£ãåå¾ãã¦ãã¾ãã
ãããã«
SwiftUIã«ã¯ãã¤ã³ãã£ã³ã°ã®ä»çµã¿ãæ¨æºçã«å®è£ ããã¦ãããUIKitã¨RxSwift(RxCocoa)ã§ã®å®è£ ã«ããã¹ãããªããã£ããã¨æ¸ããã¨ãã§ãã¾ããã ã¾ããä»åå®è£ ããViewModelã¯Viewå´ãUIKitã«å·®ãæ¿ãã¦ã使ããã¨ãã§ããããï¼ãã¤ã³ãã£ã³ã°ã®ä»çµã¿ãç¡ãã®ã§å°ãã¤ããã§ããï¼ã SwiftUIã§ã¯ã¾ã å®è£ ãå³ããé¨åã«ã¤ãã¦ã¯ãä¸é¨UIKitã§å®è£ ãããªã©ã®é¸æè¢ãåããã¨ãå¯è½ãã¨æãã¾ãã
Combineã«ã¤ãã¦ã¯ã2019å¹´12ææç¹ã§ã¯WithLatestFrom
ãªãã¬ã¼ã¿ãç¡ããªã©ã®åé¡ã¯ããã¾ããã
RxSwiftã触ã£ããã¨ãããã°ãããªãå®è£
ã§ãããã¨æãã¾ãã
SwiftUIã¨Combineã®ããããã«æå¾
ãã¦ãã¾ãï¼