Pull to refresh
18
0
Александр @SparkLone

iOS

Send message
Я в итоге остановился на ForkLift, есть триал, можно затестить.
Голь на выдумку хитра, зачет )

P.S. Да читаем мы теги, читаем )
Спасибо за статью. Отлично все разобрано.
Надеюсь оправдать доверие партии )
Здравствуйте, Святослав )
Времени статьи отняли конечно порядочно, но просто захотелось хоть немного вернуть долг сообществу. Ведь сколько было за эти годы прочитано статей. Есть время разбрасывать камни, и время собирать камни )
Спасибо Вам за ответы.
По поводу вопросов 2 и 3. Для 3ей статьи я как раз создал приложение имеющее идентичный функционал но с разными архитектурными подходами. Производительность правда там особо не померяешь, не те нагрузки. А вот по поводу веса вполне можно было бы сказать, но нужно делать полноценный билд для App Store, там же собирается «по уму», то что не нужно отрезается и все в таком духе.
4) Золотые слова. Я как раз слишком увлекся Rx-фикацией, когда пытался сделать clear Rx везде где только можно. Урок был усвоен )

В последнее время я заинтересовался immutable моделями, судя по WWDC 2016, Apple тоже стала её продвигать. Но тут своих нюансов тоже хватает, как и в Rx. Если в итоге паззл в голове сложится — будет и 3я статья )
Рад, что мои статьи действительно помогают, а не просто осели в избранном )
До юнит тестов у меня руки не дошли (планировал 4й в цикле статей), так что Ваша статья — очень нужна.
К сожалению 3я статья в состоянии «заморозки», она была посвящена архитектурным решениям RxSwift в сравнении с другими (MVC, MVVM без RxSwift). Но в процессе написания пришло понимание, что у меня не хватает времени и опыта практической работы с RxSwift, чтобы написать действительно достойный материал, а писать «чтобы было» не хочется.
В комментариях к другой статье по RxSwift я попробовал попытать автора на интересовавшие меня вопросы, но обьем текста в вопросах стал приближаться к обьему написанной им статьи и общение как то сошло на нет )

Так что сделаю еще одну попытку затронуть интересующие темы, если позволите.
1) Вы, я так понимаю, активно применяете RxSwift в production коде. Это приложения для клиентов или для себя? Если для клиентов — как они смотрят на то, что код будет «нестандартным»?
2) Не сталкивались ли с проседанием производительности в RxSwift?
3) Сколько добавляет в весе использование RxSwift в релизе?
4) RxSwift используется повсеместно во всем приложении или в основном для GUI привязок?

Просто RxSwift подразумевает под собой реализацию парадигмы функционально-реактивного программирования, которая включает в себя неизменяемость, что довольно тяжело выполнить везде. Достаточно посмотреть на их попытку продемонстрировать работу с TableView в RxSwift (TableViewWithEditingCommands).
Так что часто приходится в VM использовать изменяемые модели.
А вообще беда в том, что технология все же еще молода, и нет нормально выкристализованных best practice. Хотя мне очень понравилось работать с RxSwift, так что через время наверное сделаю еще одну попытку.
Ну у меня была задача обновлять процент скачанного в прогресс барах как основного контроллера так и в отдельном экране, поэтому сделал модель структурой, положил ее в в Variable, и в сервисе отдавал именно в обертке. При изменении сервисом процента в структуре — viewModel подписанные на изменение Variable — обновляет все свои переменные, которые по биндингам уходят во ViewController.
Это позволило оставить модель «тупой».
Из минусов подхода.
Если модель будет классом — придется после каждого изменения в сервисе делать updateModel (переприсваивать саму себя в переменную)
Если модель будет структурой — изменение любого свойства — автоматом обновит всю модель о чем узнает ViewModel. Но если надо поменять последовательно 10 свойств — каждое изменение будет обновлять всю модель. В качестве обходного пути — сначала достать структуру из переменной, обновить все в ней — и только потом присвоить обратно.

Кстати не заметно просадки по производительности при использовании RAC? И сколько добавляет в весе программы использование этого фреймворка?
Да уж, не маленький, накипело ))
1) под переменной подразумевается Variable из RxSwift, класс позволяющий напрямую засунуть в нее данные, не из потока Rx. Понятно что по сути все переменные Observable динамически меняют данные в зависимости от того что на входах к которым они подсоединены, но это вписывается в dataFlow. А тут как бы точка выхода в обычный мир, не реактивный.
2) Да я понимаю, что для всего есть фреймворки. Но изначально хотелось понаступать в pet проекте на все грабельки, чтобы собрать опыт и потом знать чего не хватает для полного счастья.
3) Проблема не столько из за использования структур, сколько от в принципе попытки делать обертку над NSManagedObject. Проблемы когда у NSManagedObject'ов есть связи, их приходится оставлять в виде objectid, грубо говоря не deep копирование. Иначе при создании одного объекта — он рекурсивно начнет создавать все до чего дотянется. В CoreData то используется Faulting.

В Service передаете те же data модели что и во viewModel, или вы конвертируете их в облегченные, чтобы не тащить все?
Между Model и ViewModel есть биндинги? Соответственно когда Service что то меняет — он отдает это в Rx виде или просто кусок данных? Или меняет model внутри себя, а модель благодаря биндингам уведомляет всех включая viewModel?
Если через биндинги, то на каком этапе фильтруете данные? Если Service что то хочет поменять в Model, но эти данные не валидны — он сам это определяет и в model всегда корректные данные, или один из подписчиков на model, какой нибудь валидатор это разруливает?

p.s. да я понимаю, что это гонка за единорогом, сделать все Правильно (с) Но по большому счет напрягает именно необходимость тащить фреймворк в зависимость. Работая в конторе — одно дело, там проекты побольше, планирование, наработанные практики. В одиночку надо пройти этот путь на паре своих проектов прежде чем предлагать клиенту.
Спасибо за статью.
Я пока подзаморозил написание своей по применению RxSwift, хоть она уже и была написана на половину, т.к. столкнулся с вопросами на которые у меня у самого нет «красивых» ответов.
Т.к. RxSwift, RAC — это реализации идеи функционального программирования — разумно использовать их мощь на полную.
Я же в итоге пришел к тому, что Rx отлично вписывается в биндинг с UI, но постоянно попадаются подводные камни.

Главная загвоздка в том, что идея реактивного программирования является — отсутствие состояния (в идеале). Отсюда идут настоятельные рекомендации подписываться на Observable только из UI (соответственно disposeBag должен быть только в ViewController'ах), т.к. подписки автоматически освободятся когда сменится экран. Т.е. та же ViewModel не должна иметь состояния.

Классическая схема MVVM если один контроллер создает другой
ParentViewController -> ParentViewModel -> ParentModel
ChildViewController -> ChildViewModel -> ChildModel

Причем ViewController знает о ViewModel, но не создает. А ParentViewModel — создает СhildViewModel
На все подписки по хорошему мы должны подписываться только в View
Ну и использовать переменные крайне не рекомендуется, только Observable только хардкоръ.

Но рассмотрим простую ситуацию.
Есть ViewController содержащий элементы логина + TableViewController (посредством контейнера).
Опять таки, по канонам ViewModel для TableViewController должен создавать ViewControllerModel.

ViewControllerViewModel когфигурируется из UI

class ViewControllerViewModel {
	func configure(login login: Observable<String>, password: Observable<String>, moduleId: Observable<String>, buttonParse: Observable<Void>) {
		...
		// внутри после всех проверок логина на пустоту и прочего в итоге мы получаем что то вроде этого

		let items: Observable<[DownloadTaskModel]> = parseState
		            .flatMapLatest{ (state) -> Observable<[DownloadTaskModel]> in
		                if case .Success(let data) = state {
		                    return Observable.just(data)
		                } else {
		                    return Observable.just([])
		                }
		            }.startWith([])
		        
		// и тут нам надо передать данные для TableViewModel, и мы не можем подписываться отсюда ни на что, т.к. мы внутри viewModel. Можно вытащить observable наружу, чтобы ViewController этой ViewModel "впустую" на него подписался, но это грабли.
		        tableViewModel.configure(items)

	}
}


У TableViewController же получается отрабатывает viewDidLoad раньше чем у его родителя ViewController, поэтому к моменту загрузки таблицы — у нас еще нет для нее данных, и приходится опять таки городить грабли

class TableViewControllerViewModel: NSObject, TableViewControllerViewModelProtocol {
    var session: NSURLSessionProtocol
    
    var disposeBag = DisposeBag()
    
    var dataSourceItems: Driver<[NumberSection]>! {
        didSet {
            completeConfigure.onNext()
        }
    }
    
    let completeConfigure: PublishSubject<Void> = PublishSubject()
    
    init(session: NSURLSessionProtocol = SessionFactory.sharedInstance.backgroundSession()) {
        self.session = session
        super.init()
    }
    
    func configure(items: Observable<[DownloadTaskModel]>) {
        
        dataSourceItems = items.asObservable().doOnNext{ [unowned self] _ in
            self.disposeBag = DisposeBag()
        }
            .map{[unowned self] models -> [NumberSection] in
                let protocols = models.map{ model -> DownloadableTaskViewModelProtocolThing in
                    let downloadDelegate = Downloader(session: self.session)
                    downloadDelegate.update.subscribe().addDisposableTo(self.disposeBag)
                    let cellViewModel = DownloadTaskViewModel(model: model, downloadDelegate: downloadDelegate)
                    downloadDelegate.updateStatusIfAlreadyDownloaded()
                    return DownloadableTaskViewModelProtocolThing(thing: cellViewModel)
                }

            }.asDriver(onErrorJustReturn: [])
    }
}


В TableViewController приходится подписываться на completeConfigure, и только после получения сигнала — конфигурировать dataSource для таблицы.

Есть альтернатива — делать dataSourceItems как переменную, изначально пустую и в configure — присваивать значение этой переменной.
Но я перечитал все issue где разработчик RxSwift отвечал на вопросы, и он черным по белому говорил — если вам приходится использовать переменную, либо вы что то делаете неправильно, либо у вас нет другого выхода. ViewModel не должна иметь состояния.
И несколько устаешь каждый раз думать первый это случай или второй.

Так же мне очень не нравится, что внутри ViewModel есть DisposeBag. А нужен он потому что нам надо подписываться на Observable update контроллера по закачке. Пробрасывать в TableViewController не получится, т.к. при каждом переконфигурировании данных для таблицы надо старую подписку сбрасывать.

Второй пример.
Есть DownloadController, его задача реагировать на нажатие кнопки у закачки соответственно,

В классическом варианте мы имеем что то вроде такого

class DownloadController {
...
func download() {
    switch model.status {
    case .Progress, .Wait:
        model.status = networkTaskManager.pause()
    case .Stopped, .Error:
        model.status = .Wait
        let task = session.downloadTaskWithURL(assetUrl())
        networkTaskManager.startPrepareTask(task)
    case .Paused:
        if let data = networkTaskManager.resumeData {
            model.status = .Wait
            let task = session.downloadTaskWithResumeData(data)
            networkTaskManager.startTask(task)
        } else {
            let task = session.downloadTaskWithURL(assetUrl())
            networkTaskManager.startTask(task)
        }
}
}


Model имеет биндинги, поэтому мы спокойно обновляем ее состояние, которое пробрасывается в ViewModel.

Когда я попробовал избавиться от состояния, то столкнулся с тем что код лишь усложнился. Как видно изменение статуса — не только результат операции — но и может меняться перед и во время операции, поэтому просто представить это как dataFlow уже не получается. Приходится делать постоянные сайд эффекты .doOnNext{ model.status =… } что снова таки по словам разработчика RxSwift — ugly hack, применение которого означает что вы что то делаете не так.
И в итоге получилось лишь усложнение.

Были еще моменты и не мало. Написал то что сразу в памяти всплыло.
Я скорее всего просто не умею готовить правильно FRP, признаю. Наверное это и есть steep learning curve. Но когда я понял, что я трачу 80% времени на то чтобы сделать «правильно по канонам FRP» вместо того чтобы писать полезный код, я отступил назад.
В итоге вернулся на шаг назад, — сделал самописный Bind, для простых задач связать Model->ViewModel->ViewController его хватает. Я конечно потерял много плюшек от RxSwift. Но признаться меня еще как то внезапно напрягло то, что это все таки не пара классов, это фреймворк, который пока мало кто использует. Как и саму парадигму FRP.
Т.к. в данный момент я лишь планирую выйти на рынок как фрилансер наверное было бы глупо уходить с головой в FRP, сомневаюсь что это нужно на небольших проектах, а как обьяснить клиенту, зачем я буду подключать ему целый фреймворк я пока не знаю.
Так что я пока взял паузу в наших отношениях с RxSwift.
При этом я ценю идею immutable заложенную в основу FRP, и данные сейчас стараюсь хранить в структурах, а не классах. Это тоже накладывает ограничения, и доставляет определенные неудобства (особенно весело с CoreData). Но их намного проще обойти и польза от них наглядная, причем все это бесплатно, без фреймворков.
Я не говорю, что FRP — плохо, но надо потратить не мало времени, чтобы умело пользоваться его арсеналом, при этом научиться обходить его ограничения.
Я пока остановился на модифицированном MVVM: (Router — ViewController — ViewModel — Model + Service Layer) При этом Model — структуры, что позволяет контролировать изменение данных. Ну и как я говорил — самописный биндинг.
Не знаю в тему ли будет мой ответ по наушникам, но поделюсь опытом. Очень хорошей звукоизоляцией (именно изоляцией, шумодава в них нет) обладают Beyerdynamic dt 770 pro, поэтому я собственно их и выбрал, это по сути полупроф наушники, часто вижу их на фотках у радиоведущих. Я же работаю дома, рядом дети, и это в плане шумов будет похлеще любого офиса. Действительно спасают, если отвлекает музыка — включается шум дождя и растворяешься в коде (природный шумодав).
Так же огромным преимуществом (для меня так точно) — являются велюровые амбушюры, просто практически у всех наушников со звукоизоляцией используется кожа или что то вроде того, и после часа работы уши оказываются в жаркой бане.
Минусом наверное можно назвать цену, но если смотреть сетап из статьи, лишние 15 тысяч, вроде особой погоды не сыграют.
Правда после покупки они мне прилично давили, на ночь поставил на растяжку и с тех пор никаких проблем.
Ну что тут скажешь, всегда можно найти «Верёвку достаточной длины, чтобы выстрелить себе в ногу» (с)
Ну я поэтому и сказал, что решать только в частном порядке.

Учитывая что идет сравнение по адресу памяти — крайне сомневаюсь, что получится что то сделать. Если бы была такая возможность, — просто представьте какие возможности это открыло бы перед злоумышленниками.
Это бы означало, что все обязаны знать про Rx, я думаю авторы других библиотек не особо обрадуются такому повороту.
Скорее если уж вводить общий API — определить, что если мы подменяем delegate то у прокси обязательно должен быть метод oldDelegate() -> NSObject
И каждая библиотека пусть при подмене делегата смотрит — есть ли такой метод, если есть — рекурсивно получает делегат, и если в итоге понимает что она уже есть где то глубже — ничего не подменять.
Но тут две проблемы.
1) это же надо ввести как стандарт, заставить авторов это реализовать
2) для какой то библиотеки может быть не критично насколько глубоко она стоит в иерархии oldDelegate, а для какой то обязательно надо быть первой. И рано или поздно найдутся библиотеки для которых кровь из носу надо быть первыми. И мы получим ту же проблему.

В общем каждый случай нужно рассматривать отдельно.
Для конкретных двух библиотек решение найти не проблема, вопрос в том как решить в общем виде. Ведь таких библиотек может быть и 20, в каждую вносить знание о каждой — никто не станет даже браться.
Приводить к единому стандарту? Опять таки вряд ли кто займется.
Ну значит я в верном направлении ковыляю.
Жду статью )
С удовольствием почитаю. Статья основанная на успешном опыте будет крайне кстати.
Если вкратце, — MVVM?
Я вот сейчас как раз думаю как по уму это все применить. MVVM, + вынес все таки роутер из ViewController'а. Не очень нравится, что из VM в какой то мере получается god объект, он хоть и делегирует все сервисам, но все таки это уже не совсем чистый VM. Думаю как грамотно распилить VM и стоит ли это делать.
Ну то, что прокси переустанавливается при несоответствии при каждой Rx манипуляции было в принципе освещено в статье в комментариях к содержимому функции proxyForObject
По поводу того баг ли это или нет я затрудняюсь сказать. Точнее понятно, что это потенциальная проблема. Но вот как решить. Можно дать возможность настраивать ядро Rx, чтобы какие то классы и их производные не перезаписывались проксей (в надежде что они уже выставили нашу прокси как свой forwardToDelegate). Не самый лучший подход. Можно кешировать rx_delegate, но тогда нужно следить за временем жизни прокси самому, ну или если после этого сверху что то было навернуто посредством BlocksKit, то наша прокси будет об этом не знать и все будет пролетать мимо BlocksKit. В общем простого решения нет, причем это не проблема Rx, а в принципе библиотек которые вмешиваются в работу runtime'а
Ведь в таком случае баг можно постить и на пресловутый BlocksKit.
Ммм, да вроде в стандартном, RxSwift

RxSwift/RxCocoa/Common/_RXDelegateProxy.m

RxSwift/RxCocoa/Common/_RXObjcRuntime.m

Поиском на гитхабе в репозитории RxSwift находится без проблем

Стоит посмотреть на реализацию в _RXDelegateProxy.m. Если вкратце, — при создании прокси извлекаются все доступные методы и выставляются в качестве доступных. Так же в _RXObjcRuntime.m производится swizzle методов отвечающих за диспетчеризацию.
Таким образом при возникновении события — отрабатывает перехватчик из _RXDelegateProxy

-(void)forwardInvocation:(NSInvocation *)anInvocation {
    if (RX_is_method_signature_void(anInvocation.methodSignature)) {
        NSArray *arguments = RX_extract_arguments(anInvocation);
        [self interceptedSelector:anInvocation.selector withArguments:arguments];
    }
    
    if (self._forwardToDelegate && [self._forwardToDelegate respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self._forwardToDelegate];
    }
}


Если сигнатура метода есть в нашем списке — вызывается interceptedSelector уже для DelegateProxy

public override func interceptedSelector(selector: Selector, withArguments arguments: [AnyObject]!) {
    subjectsForSelector[selector]?.on(.Next(arguments))
}


subjectsForSelector же заполняется нужными сигнатурами, только когда мы делаем observe для delegate прокси.

public func observe(selector: Selector) -> Observable<[AnyObject]> {
        if hasWiredImplementationForSelector(selector) {
            print("Delegate proxy is already implementing `\(selector)`, a more performant way of registering might exist.")
        }

        if !self.respondsToSelector(selector) {
            rxFatalError("This class doesn't respond to selector \(selector)")
        }
        
        let subject = subjectsForSelector[selector]
        
        if let subject = subject {
            return subject
        }
        else {
            let subject = PublishSubject<[AnyObject]>()
            subjectsForSelector[selector] = subject
            return subject
        }
    }


Таким образом подписываемся мы вроде на все события, но DelegateProxy обрабатывает лишь те на которые есть подписка.

Далее вне зависимости от того отработал код в DelegateProxy или нет — если в _RXDelegateProxy есть forwardToDelegate — событие отправляется уже в оригинальный делегат
Вот как раз над этим сейчас и работаю. Перебираю возможные реализации. Т.к. к сожалению даже в Rx примерах архитектура на мой взгляд говоря не ахти, хоть там и заявлен MVVM, но создавать из ViewController — ViewModel, причем передавая в инициализацию сервисы — так себе идея. Ищу баланс между чистотой кода, расширяемостью и количеством классов на отдельный юнит.

Information

Rating
Does not participate
Date of birth
Registered
Activity