Ссылка на свойства содержащего класса при использовании внутренних структур в swift

#swift #mvvm #protocols

#swift #mvvm #протоколы

Вопрос:

Я рефакторингую проект для использования MVVM и использую протоколы, чтобы гарантировать, что мои модели представления имеют согласованную структуру. Это отлично работает для определения общедоступных свойств, относящихся к вводу и выводу (которые основаны на внутренних структурах), но определение действий таким же образом оказывается проблематичным, поскольку в настоящее время они определяются как замыкания, которые должны ссылаться на свойства модели просмотра. Если я использую тот же подход, что и для ввода и вывода свойств, я не думаю, что смогу получить доступ к свойствам содержащего экземпляра.

Пример:

 protocol ViewModelType {
    associatedtype Input
    associatedtype Output
    associatedtype Action
}

final class MyViewModel: ViewModelType {
    struct Input { var test: String }
    struct Output { var result: String }
    struct Action { 
        lazy var createMyAction: Action<String, Void> = { ... closure to generate Action which uses a MyViewModel property }
    }
    var input: Input 
    var output: Output
    var action: Action
}
  

Это не нарушает условия сделки, если я не могу этого сделать, но мне было любопытно, поскольку я не вижу никакого способа получить доступ к родительским свойствам.

Ответ №1:

Ответ на ваш вопрос

Давайте начнем с примечания, которое createMyAction: Action<String, Void> ссылается на тип ( struct ) с именем Action , как если бы он был универсальным, но вы не объявили его как таковой и, следовательно, не будет работать.

И чтобы ответить на ваш вопрос о вложенном, struct Action может ссылаться на его внешние class MyViewModel — да, вы можете ссылаться на static свойства, подобные этому:

 struct Foo {
    struct Bar {
        let biz = Foo.buz
    }
    static let buz = "buz"
}

let foobar = Foo.Bar()
print(foobar.biz)

  

Но вам, вероятно, следует избегать таких циклических ссылок. И я опущу любой уродливый взлом, который мог бы обеспечить такую циклическую ссылку на нестатические свойства (вероятно, будет включать изменяемые необязательные типы). Это запах кода.

Предложение для MVVM

Похоже, вы хотели бы объявить Action как функцию? Я сам использую этот протокол:

 protocol ViewModelType {
    associatedtype Input
    associatedtype Output
    func transform(input: Input) -> Output
}
  

Первоначально вдохновленный чистой архитектурой SergDort.

Вы можете подготовить экземпляр input (содержащий Observable классы) из UIViewController и вызвать transform функцию, а затем сопоставить Output преобразования (являющиеся Observables классами) для обновления графического интерфейса.

Таким образом, этот код предполагает, что у вас есть базовые знания о реактивности. Что касается Observable s, вы можете выбирать между RxSwift или ReactiveSwift — да, их имена похожи.

Если вас устраивает Rx, это отличный способ добиться хорошей архитектуры MVVM с помощью простых асинхронных обновлений графического интерфейса. В приведенном ниже примере вы найдете тип, Driver который задокументирован здесь, но краткое объяснение заключается в том, что это то, что вы хотите использовать для ввода из представлений и ввода в представления, поскольку он обновляет представления в потоке GUI и гарантированно не выдает ошибку.

Чистая архитектура содержит, например PostsViewModel :

 
final class PostsViewModel: ViewModelType {

    struct Input {
        let trigger: Driver<Void>
        let createPostTrigger: Driver<Void>
        let selection: Driver<IndexPath>
    }
    struct Output {
        let fetching: Driver<Bool>
        let posts: Driver<[PostItemViewModel]>
        let createPost: Driver<Void>
        let selectedPost: Driver<Post>
        let error: Driver<Error>
    }

    private let useCase: PostsUseCase
    private let navigator: PostsNavigator
    
    init(useCase: PostsUseCase, navigator: PostsNavigator) {
        self.useCase = useCase
        self.navigator = navigator
    }
    
    func transform(input: Input) -> Output {
        let activityIndicator = ActivityIndicator()
        let errorTracker = ErrorTracker()
        let posts = input.trigger.flatMapLatest {
            return self.useCase.posts()
                .trackActivity(activityIndicator)
                .trackError(errorTracker)
                .asDriverOnErrorJustComplete()
                .map { $0.map { PostItemViewModel(with: $0) } }
        }
        
        let fetching = activityIndicator.asDriver()
        let errors = errorTracker.asDriver()
        let selectedPost = input.selection
            .withLatestFrom(posts) { (indexPath, posts) -> Post in
                return posts[indexPath.row].post
            }
            .do(onNext: navigator.toPost)
        let createPost = input.createPostTrigger
            .do(onNext: navigator.toCreatePost)
        
        return Output(fetching: fetching,
                      posts: posts,
                      createPost: createPost,
                      selectedPost: selectedPost,
                      error: errors)
    }
}
  

Комментарии:

1. Я «взломал» его, создав структуру (называемую Actions), содержащую общедоступные методы получения для частных ленивых переменных, которые определяли действия RxSwift, которые я искал. Это работает, но я чувствовал себя некомфортно, делая это, что, вероятно, было моим подсознанием, предупреждающим меня о запахе кода. Я думаю, проблема заключалась в том, что я пытался рассматривать действия как отдельную сущность в своем собственном праве, а не как выходные данные виртуальной машины (что, конечно, так и есть). Спасибо за указатель — я проведу рефакторинг и посмотрю, устранит ли это другую проблему, с которой я только что столкнулся…

2. дайте мне знать, если вам нужны дополнительные данные. Вы могли бы взглянуть на это приложение для кошелька iOS для криптовалюты Zilliqa под названием Zhip, которое я написал с открытым исходным кодом. Zhip использует этот transform(input) -> Output шаблон в своих ViewModels. github.com/OpenZesame/Zhip