Skip to content

Instantly share code, notes, and snippets.

@Lavmint
Last active September 7, 2021 07:58
Show Gist options
  • Save Lavmint/d085fbb2d6f37b890cb2478c8937bb75 to your computer and use it in GitHub Desktop.
Save Lavmint/d085fbb2d6f37b890cb2478c8937bb75 to your computer and use it in GitHub Desktop.
Swift UI FAQ

Swift UI FAQ

Данный документ актуален по состоянию на стабильный XCode 11.1

NavigationView

Не освобождается память после pop

На iPhone из NavigationView осуществляется push переход через NavigationLink в destination. После возврата pop, видим что память, которую занимал detination, не освободилась.

Дело в том что по умолчанию NavigationView использует DoubleColumnNavigationViewStyle, который под капотом превращается в SplitViewController. Он то и удерживает сильную ссылку на destination.

Нужно явно указать StackNavigationViewStyle для iPhone. В случае если на iPad нужен SplitViewController, то нужно указать это условием при описании структуры View

NavigationView {
    NavigationLink(destination: Text("destination"), label: {
        Text("push")
    })
}
.navigationViewStyle(StackNavigationViewStyle())

ObservableObject

Ссылка на объект каждый раз разная

class ViewModel: ObservableObject { }

struct RootView: View {
    
    var body: some View {
        VStack {
            Text("Parent")
            ContentView()
        }
    }
}

struct ContentView: View {
    
    @ObservedObject var viewModel = ViewModel()
    
    var body: some View {
        Text("Content")
    }
    
}

При обновлении RootView его body пересоздается, создавая новое описание ContentView с новым экземпляром ViewModel. Чтобы этого не происходило - нужно где-то хранить ссылку на ViewModel.

Хранить ее можно в UIHostingController.

struct ContentView: View {
    
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        Text("Content")
    }
    
}

let vc = UIHostingController(rootView: ContentView(viewModel: ViewModel()))

Далее нужно как-то дать возможность этот контроллер описывать в struct view.

public protocol ViewModelProtocol: class {
    static func instanceInView() -> UIViewController
    var bindings: Set<AnyCancellable> { get set }
    func onAppear()
    func onDisappear()
}

extension ViewModelProtocol {
    
    func bind(uiViewController: UIViewController) {
        uiViewController.publisher(for: \.parent)
            .sink(receiveValue: { [weak self] (parent) in
                if parent == nil {
                    self?.bindings.cancel()
                }
            })
            .store(in: &bindings)
    }
    
}

struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
        return ViewModel.instanceInView()
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
        //
    }
}
class ViewModel: ObservableObject {
    
    static func instanceInView() -> UIViewController {
        let vm = ViewModel()
        let vc = UIHostingController(rootView: ContentView(viewModel: ViewModel))
        vm.bind(uiViewController: vc)
        return vc
    }
    
}
struct RootView: View {
    
    var body: some View {
        ModelView<ContentViewModel>()
    }
}

Как удерживается ссылка:

graph LR
A[parent: UIViewController?] -- strong --> B[UIHostingController]
B -- strong --> C[ContentViewModel]
B -- weak --> A
A -- cancel bindings on nil --> C

Отмену подписок можно и не делать, но в таком случае нужно чтобы все sink в ViewModel захватывали слабую ссылку на ViewModel, чтобы не писать лишний код и не думать о retain cycle проще отписать просто все, правда?)

SHEET, POPOVER, ALERT, ActionSheet

struct ContentView: View {
    
    @State var isPresented: Bool = false
    
    var body: some View {
        VStack {
            Button(action: {
                self.isPresented = true
            }, label: {
                Text("Present")
            })
            Text("Content")
                .sheet(isPresented: $isPresented, content: {
                    Text("Will never be shown")
                })
        }
        .sheet(isPresented: $isPresented, content: {
            Text("Sheet")
        })
    }
    
}

В данном примере sheet компонента Text никогда не будет показан, так как VStack определил sheet уровнем выше.

PickerView

Form

При встраивании в Section of Form ломается поведение navigationTitle. Text указанный при создании PickerView не уходит в navigationTitle.

Нажатие на элементы списка, который создался автоматически при описании PickerView происходит только непосредственно в том месте где находится текст, а не по всей ширине сроки.

Standalone

При создании PickerView отдельно его Text постоянно присутствует на экране и отнимает место у крутилки.

TextField

Не работает инициализатор, который принимает formatter, текст просто не форматируется и не изменяется.

Даже если написать свою логику форматирования - после каждого форматирования будет смещаться курсор.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment