[Программирование, Разработка под iOS, Разработка мобильных приложений, Swift] Приложение на SwiftUI в AppStore – сложности разработки
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
SwiftUI – это молодая и пока что не совсем изученная технология. С одной стороны, большое пространство для творчества и исследования, а с другой – неизвестность, нестабильность и проблемы.Так ли просто писать на SwiftUI, как показывают на WWDC? Я расскажу о сложностях, с которыми столкнулся лично я во время написания собственного приложения. Оно полностью написано на SwiftUI и выложено в App Store.Какие проблемы могут встретиться во время разработки? Давайте разбираться.Проблема конструктора View Главная и самая большая проблема, которая накладывает ограничения почти на любой архитектурный подход. Мое приложение построено на MVI, поэтому рассматривать буду в рамках этой архитектуры. Подробнее об MVI я писал в публикации MVI и SwiftUI – одно состояниеСначала сделаю небольшое отступление:@ObservedObject и @StateObjectВ MVI есть модуль, который отвечает за бизнес логику, называется Intent. У View на Intent есть ссылка.
struct ContentView: View {
@ObservedObject var intent: ContentIntent
var body: some View {
Text("Hello, world!")
}
}
@ObservableObject нужен для того, чтобы можно было отслеживать все, что происходит в классе и сообщать об этом View, а тот, в свою очередь, опираясь на новые данные, меняет отображение UI элементов.А теперь к проблеме.View в SwiftUI обладает особенностью — когда требуется перерисовать View, он пересоздается, в таких случаях init вызывается повторно. Как правило, View пересоздается, когда у другой View, которая стоит по иерархии выше, меняются данные.Давайте рассмотрим пример ниже, где одна из View постоянно пересоздается.
// MARK: - Screen ContentView
struct ContentView: View {
@State var isNextScreenVisible = false
@State var seconds = "0"
var body: some View {
NavigationView {
VStack(spacing: 16) {
Text("Seconds: \(seconds)")
NavigationLink("Next screen", destination: NextView(), isActive: $isNextScreenVisible)
}
}.onAppear {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.seconds = String(Int(Date().timeIntervalSince1970) % 60)
}
}
}
}
// MARK: - Scren NextView
struct NextView: View {
@ObservedObject var intent: NextIntent
init() {
intent = ContentIntent()
print("init NextView")
}
var body: some View {
Text("Hello Weold!")
}
Если открыть экран NextView, то конструктор у него будет вызываться каждую секунду. В консоли мы увидим:
init NextView
init NextView
init NextView
init NextView
init NextView
init NextView
init NextView
init NextView
init NextView
Каждый раз когда вызывается init, все объекты внутри View пересоздаются и модуль, отвечающий за бизнес логику (в нашем случае Intent), также будет создан заново, все данные в нем сбросятся. В любой момент экран может вернуться к своему первоначальному состоянию, т. е. к состоянию на момент открытия.Чтобы этого не происходило, @ObservedObject можно заменить на @StateObject и тогда Intent перестает зависеть от жизненного цикла View. Как это работает? Каждый раз когда View пересоздается, у всех модулей в нем вызывается конструктор (в MVI это Intent и Model, в MVVM это будет ViewModel).
После того, как Intent инициализирован, он помещается в контейнер @StateObject; если там уже лежит объект Intent, то новый созданный Intent удаляется. @StateObject очень похож на Singleton, только он удаляется из памяти.
Учитывая, что конструктор будет вызываться у Intent постоянно, даже если он будет @StateObject, в init у Intent не стоит писать запросы и любую другую логику кроме инициализации объектов. @StateObject доступен с версии iOS 14.
Можно ли использовать @ObservedObject, но сделать его Singleton?Допустим, во время инициализации мы будем подкладывать уже готовый объект.
struct ContentView: View {
@ObservedObject var intent: ContentInten
init() {
self.intent = ContentInten.shared
let model = ContentModel()
self.intent.update(model: model)
}
var body: some View {
Text("Hello, world!")
}
Одна проблема закроется, но возникнут другие:
- При передаче данных в Intent при инициализации будут возникать вопросы (существуют ли сейчас данные, если они уже есть, нужно ли их перезаписывать и т. д.)
- Singleton мы не можем уничтожить и он будет держаться в памяти всю сессию приложения
- При создании второго, третьего и других последующих экранов у нас будет один Singleton на все эти экраны.
SwiftUI в iOS 14 сильно расширен, чтобы не бороться с этими проблемами и получать больше возможностей, есть смысл делать минимальную версии iOS 14 для экранов SwiftUI.Объект @StateObject нельзя инициализировать внутри init View
Еще одна особенность. Intent должен быть инициализирован вне конструктора View. Для этого я использую статическую функцию, в которую, если нужно, можно передать данные.
struct ContentView: View {
@StateObject var intent: ContentInten
var body: some View {
Text("Hello, world!")
}
static func build() -> some View {
let model = ContentModel()
let intent = ContentInten(model: model)
return ContentView(intent: intent)
}
В общем, проблема множественного вызова конструктора создает много неудобств и накладывает большие ограничения на архитектуру для iOS 13.Проблема onAppear и onDisappearМы не знаем, когда View удален и пользователь его не видит, или наоборот, когда View впервые показан. Есть мнение что, что onAppear и onDisappear вызываются один раз, когда экран создается и когда он удаляется. Это не так. И тот и другой метод может вызваться более одного раза у одного экрана. Давайте посмотрим пример
struct ContentView: View {
@State var isVisibleHomeScreen = false
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
NavigationLink(destination: Text("Screen Home"),
isActive: $isVisibleHomeScreen,
label: { Text("Open screen") })
}.onAppear {
print("onAppear was called")
}.onDisappear {
print("onDisappear was called")
}
}
}
При открытии и закрытии экрана внутри NavigationView эти методы срабатывают более одного раза. В консоли мы увидим:
onDisappear was called
onAppear was called
Как правило, NavigationView указывается у одного экрана, первого, а у всех последующих уже нет. onAppear и onDisappear будут срабатывать в последующих экранах больше одного раза. Это надо держать в голове при реализации каких-либо архитектурных решений.НавигацияКак организовать навигацию в приложении со SwiftUI? Этот вопрос стоит особо остро, так как навигация тут работает по другим правилам, и весь накопленный опыт сообщества работы с UIKit тут не работает. Можно найти немало статей и видео, посвященных теме навигации в SwiftUI. Все, что я видел, или неудобно, или громоздко. Я написал свой вариант, чтобы переходы делались ровно так, как нам показывали на WWDC. Это оказалось несложно, всю логику навигации нужно было вынести в отдельный "some View". Во время разработки приложения этот вариант роутера показал себя очень хорошо, даже лучше, чем MVI. Так как это большая тема, под сполером я оставлю пример, а сам подход, возможно, опишу в другой статье.Router
// ContentRouter.swift
//
// Copyright © 2020 Vyacheslav Ansimov. All rights reserved.
//
import SwiftUI
import Combine
// MARK: - Realization
struct ContentRouter: View {
enum ScreenType {
case sheetScreen(value: String)
case navigationScreen(value: String)
case exit
}
private class ScreenTypeHolder: ObservableObject {
@Published var type: ScreenType?
}
// API
let screen: PassthroughSubject<ScreenType, Never>
// private
@Environment(\.presentationMode) private var presentationMode
@StateObject private var screenType = ScreenTypeHolder()
// Life cycle
var body: some View {
displayView().onReceive(screen) { self.screenType.type = $0 }
}
private func displayView() -> some View {
let isVisible = Binding<Bool>(get: { screenType.type != nil },
set: { if !$0 { screenType.type = nil } })
// Screens
switch screenType.type {
case .sheetScreen(let value):
return AnyView(
Spacer().sheet(isPresented: isVisible) {
Text(value)
}
)
case .navigationScreen(let value):
return AnyView (
NavigationLink("", destination: Text(value), isActive: isVisible)
)
case .exit:
presentationMode.wrappedValue.dismiss()
return AnyView(EmptyView())
case .none:
return AnyView(EmptyView())
}
}
}
// MARK: - Example
struct ContentRouter_Previews: PreviewProvider {
static let routeSubject = PassthroughSubject<ContentRouter.ScreenType, Never>()
static var previews: some View {
NavigationView {
VStack {
Button(action: {
self.routeSubject.send(.sheetScreen(value: "Hello World!"))
}, label: { Text("Display Sheet Screen") })
Button(action: {
self.routeSubject.send(.navigationScreen(value: "Hello World!"))
}, label: { Text("Display NavigationLink Screen") })
}
.overlay(ContentRouter(screen: routeSubject))
}
}
}
Проблема проксиProperty wrapper (State, Binding, ObservedObject и др.) дают SwiftUI реактивности и делает удобным обновление UI.А если мы хотим вынести эти свойства в отдельный класс. Тогда создается класс, подписывается под протокол ObservableObject и после этого его можно использовать во View
// MARK: Model
class ContentModel: ObservableObject {
@Publised var title = "Hello World!"
}
// MARK: View
struct ContentView: View {
@ObservedObject var model: ContentModel
var body: some View {
Text(model.title)
}
}
А если нужно этот класс со свойствами перенести в другой модуль, другой класс? Тут возникают сложности.Давайте рассмотрим пример с Intent. Наш класс со свойствами будет находиться там, но при этом все изменения свойств должен видеть View.
// MARK: View
struct ContentView: View {
@StateObject var intent: ContentIntent
var body: some View {
Text(intent.model .title).onAppear {
self.intent.onAppear()
}
}
}
// MARK: Intent
class ContentIntent {
let model: ContentModel
...
func onAppear() {
model.title = "Hello World!"
}
}
// MARK: Model
class ContentModel: ObservableObject {
@Published var title = "Loaded"
}
В примере, UI элемент Text не будет получать актуальные данные и пользователь будет видеть надпись "Loaded", даже после того как все функции будут вызваны. Даже если поменять
let model: ContentModel
на
@Published var model: ContentModel
Работать не будет. ObservableObject у Model передает событие в Intent о том, что у него что-то изменилось, а не во View. Для того, чтобы View узнал что в Model что-то поменялось, событие нужно передать из Intent дальше. Чтобы заработало, нам нужно написать дополнительный код в конструкторе Intent и сам класс подписать под протокол ObservableObject.
import Combine
class ContentIntent: ObservableObject {
let model: ContentModel
private var cancellable: Set<AnyCancellable> = []
init(model: ContentModel) {
self.model = model
cancellable.insert(model.objectWillChange.sink { [weak self] in
self?.objectWillChange.send()
})
}
...
}
Когда у Model что-то меняется, вызывается метод objectWillChange, который извещает Intent о том, что в Model есть изменения. Intent этот ивент получает и вызывает у себя метод objectWillChange.send(), передавая View изменения.
Протокол ObservableObject у Intent нужен для того,чтобы можно было вызывать objectWillChange.Резимирую. Если захочется вынести свойства UI в отдельный класс и держать его не во View, нужно будет проксировать события.Проблема UIНе все, что можно сделать в UIKit, можно сделать в SwiftUI. Многих системных элементов в SwiftUI просто нет. В таких случаях приходится изобретать велосипед. А то, что есть, плохо кастомизируется.Вот несколько примеров проблем с UI элементами:
- Готового аналога UISearchBar в SwiftUI нет, придется писать логику поведения с нуля, если понадобится поисковая строка элемента.
- Когда нужен UIPageControll. В SwiftUI что-то подобное можно сделать из TabBar, но кастомизировать его не получится, придется написать свой UIPageControll.
- PikerView: нельзя уменьшить высоту элемента, он будет выходить за границы. На рисунке ниже, черной рамкой показан заданный размер PikerView. Видно, что размер элемента меньше стандартного, а фактически визуально с элементом ничего не происходит.
- TextEditor: нельзя поменять background, только белый.
И это далеко не все! Не все элементы можно изменить под свои нужды. Таких мелочей встречается очень много. Приходится или мириться, или писать свой элемент, который по поведению похож на системный. Так как системные элементы плохо поддаются кастомизации, любой шаг в дизайне не по гайдлайнам Apple будет приносить множество проблем. ЗаключениеМожно ли коммерческий проект полностью переводить на SwiftUI? Точно нет. Будут проблемы с реализацией дизайнов, будут возникать архитектурные сложности. Так как технология свежая, придется много исследовать и сталкиваться с проблемами. С другой стороны, скорость разработки UI на SwiftUI в разы выше, чем UIKit, и открываются широкие возможности работы с анимациями – в SwiftUI очень красивые анимации и делать их очень просто. Частично перевести на SwiftUI можно простые экраны, экраны категории Welcome или информационные. Здесь SwiftUI показывает себя хорошо, проблемы минимальны, а визуально с анимациями выглядит лучше, чем UIKit. Также рекомендую попробовать на своих личных проектах, не очень больших и сложных, где нет жестких требований к дизайну.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, Программирование, Исследования и прогнозы в IT, Облачные сервисы] Самые популярные языки программирования для бэкенда: для чего они подходят лучше всего и какие компании их используют (перевод)
- [Реверс-инжиниринг, Программирование микроконтроллеров, Прототипирование, Интернет вещей, DIY или Сделай сам] Подключаемся к станку по изготовлению профлиста и считываем из него прокатную длинну
- [Тестирование IT-систем, Программирование, Java, IT-стандарты, Промышленное программирование] Принцип слоеного теста
- [Разработка веб-сайтов, JavaScript, Программирование, ReactJS] Вопросы для собеседования по хукам React (перевод)
- [Программирование, Разработка мобильных приложений, Dart, Flutter] Как мы сделали миграцию пользовательских данных с нативного приложения на Flutter
- [Разработка веб-сайтов, Программирование, Dart, Flutter] DartUP 2020: итоги и видеозаписи докладов
- [Swift, Профессиональная литература] Книга «Swift. Основы разработки приложений под iOS, iPadOS и macOS. 6-е изд. дополненное и переработанное»
- [PHP, Программирование, Анализ и проектирование систем] PHP коммьюнити в СНГ. Было плохо — стало хуже
- [Программирование, IT-инфраструктура, Виртуализация, Промышленное программирование, Управление разработкой] Цифровая индустрия: непрерывная оптимизация процессов
- [Поисковые технологии, Python, Разработка мобильных приложений, Kotlin] Не баян: ищем дубликаты изображений на основе Milvus с индексом FAISS внутри
Теги для поиска: #_programmirovanie (Программирование), #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_swift, #_swiftui, #_swift, #_mvi, #_programmirovanie (
Программирование
), #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_swift
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:31
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
SwiftUI – это молодая и пока что не совсем изученная технология. С одной стороны, большое пространство для творчества и исследования, а с другой – неизвестность, нестабильность и проблемы.Так ли просто писать на SwiftUI, как показывают на WWDC? Я расскажу о сложностях, с которыми столкнулся лично я во время написания собственного приложения. Оно полностью написано на SwiftUI и выложено в App Store.Какие проблемы могут встретиться во время разработки? Давайте разбираться.Проблема конструктора View Главная и самая большая проблема, которая накладывает ограничения почти на любой архитектурный подход. Мое приложение построено на MVI, поэтому рассматривать буду в рамках этой архитектуры. Подробнее об MVI я писал в публикации MVI и SwiftUI – одно состояниеСначала сделаю небольшое отступление:@ObservedObject и @StateObjectВ MVI есть модуль, который отвечает за бизнес логику, называется Intent. У View на Intent есть ссылка. struct ContentView: View {
@ObservedObject var intent: ContentIntent var body: some View { Text("Hello, world!") } } // MARK: - Screen ContentView
struct ContentView: View { @State var isNextScreenVisible = false @State var seconds = "0" var body: some View { NavigationView { VStack(spacing: 16) { Text("Seconds: \(seconds)") NavigationLink("Next screen", destination: NextView(), isActive: $isNextScreenVisible) } }.onAppear { Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in self.seconds = String(Int(Date().timeIntervalSince1970) % 60) } } } } // MARK: - Scren NextView struct NextView: View { @ObservedObject var intent: NextIntent init() { intent = ContentIntent() print("init NextView") } var body: some View { Text("Hello Weold!") } init NextView
init NextView init NextView init NextView init NextView init NextView init NextView init NextView init NextView После того, как Intent инициализирован, он помещается в контейнер @StateObject; если там уже лежит объект Intent, то новый созданный Intent удаляется. @StateObject очень похож на Singleton, только он удаляется из памяти. Учитывая, что конструктор будет вызываться у Intent постоянно, даже если он будет @StateObject, в init у Intent не стоит писать запросы и любую другую логику кроме инициализации объектов. @StateObject доступен с версии iOS 14.
struct ContentView: View {
@ObservedObject var intent: ContentInten init() { self.intent = ContentInten.shared let model = ContentModel() self.intent.update(model: model) } var body: some View { Text("Hello, world!") }
Еще одна особенность. Intent должен быть инициализирован вне конструктора View. Для этого я использую статическую функцию, в которую, если нужно, можно передать данные. struct ContentView: View {
@StateObject var intent: ContentInten var body: some View { Text("Hello, world!") } static func build() -> some View { let model = ContentModel() let intent = ContentInten(model: model) return ContentView(intent: intent) } struct ContentView: View {
@State var isVisibleHomeScreen = false var body: some View { NavigationView { VStack { Text("Hello, world!") NavigationLink(destination: Text("Screen Home"), isActive: $isVisibleHomeScreen, label: { Text("Open screen") }) }.onAppear { print("onAppear was called") }.onDisappear { print("onDisappear was called") } } } onDisappear was called
onAppear was called // ContentRouter.swift
// // Copyright © 2020 Vyacheslav Ansimov. All rights reserved. // import SwiftUI import Combine // MARK: - Realization struct ContentRouter: View { enum ScreenType { case sheetScreen(value: String) case navigationScreen(value: String) case exit } private class ScreenTypeHolder: ObservableObject { @Published var type: ScreenType? } // API let screen: PassthroughSubject<ScreenType, Never> // private @Environment(\.presentationMode) private var presentationMode @StateObject private var screenType = ScreenTypeHolder() // Life cycle var body: some View { displayView().onReceive(screen) { self.screenType.type = $0 } } private func displayView() -> some View { let isVisible = Binding<Bool>(get: { screenType.type != nil }, set: { if !$0 { screenType.type = nil } }) // Screens switch screenType.type { case .sheetScreen(let value): return AnyView( Spacer().sheet(isPresented: isVisible) { Text(value) } ) case .navigationScreen(let value): return AnyView ( NavigationLink("", destination: Text(value), isActive: isVisible) ) case .exit: presentationMode.wrappedValue.dismiss() return AnyView(EmptyView()) case .none: return AnyView(EmptyView()) } } } // MARK: - Example struct ContentRouter_Previews: PreviewProvider { static let routeSubject = PassthroughSubject<ContentRouter.ScreenType, Never>() static var previews: some View { NavigationView { VStack { Button(action: { self.routeSubject.send(.sheetScreen(value: "Hello World!")) }, label: { Text("Display Sheet Screen") }) Button(action: { self.routeSubject.send(.navigationScreen(value: "Hello World!")) }, label: { Text("Display NavigationLink Screen") }) } .overlay(ContentRouter(screen: routeSubject)) } } } // MARK: Model
class ContentModel: ObservableObject { @Publised var title = "Hello World!" } // MARK: View struct ContentView: View { @ObservedObject var model: ContentModel var body: some View { Text(model.title) } } // MARK: View
struct ContentView: View { @StateObject var intent: ContentIntent var body: some View { Text(intent.model .title).onAppear { self.intent.onAppear() } } } // MARK: Intent class ContentIntent { let model: ContentModel ... func onAppear() { model.title = "Hello World!" } } // MARK: Model class ContentModel: ObservableObject { @Published var title = "Loaded" } let model: ContentModel
@Published var model: ContentModel
import Combine
class ContentIntent: ObservableObject { let model: ContentModel private var cancellable: Set<AnyCancellable> = [] init(model: ContentModel) { self.model = model cancellable.insert(model.objectWillChange.sink { [weak self] in self?.objectWillChange.send() }) } ... } Протокол ObservableObject у Intent нужен для того,чтобы можно было вызывать objectWillChange.Резимирую. Если захочется вынести свойства UI в отдельный класс и держать его не во View, нужно будет проксировать события.Проблема UIНе все, что можно сделать в UIKit, можно сделать в SwiftUI. Многих системных элементов в SwiftUI просто нет. В таких случаях приходится изобретать велосипед. А то, что есть, плохо кастомизируется.Вот несколько примеров проблем с UI элементами:
=========== Источник: habr.com =========== Похожие новости:
Программирование ), #_razrabotka_pod_ios ( Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_swift |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:31
Часовой пояс: UTC + 5