Handling asynchronous view controller retrieval in SwiftUI - ios

I have a flow in UIKit where, when I call a function, I retrieve a response from my API, this response is then used to create a UIViewController. Then this view controller is presented full screen:
getResponse() { result in
switch result {
case .success(let response):
let viewController = MyViewController(response: response)
presenter.present(viewController) { success in
// etc
}
case .failure:
break
}
}
I want to implement the equivalent of this in SwiftUI using a view modifier to abstract this:
.showMyView(isPresented: $isPresented)
When isPresented is true, it should get the response then present MyView. I have bits in my head of what I think I need to use, but am not sure how to piece them together.
I know that I need to use a UIViewControllerRepresentable to handle the creation of a SwiftUI version of MyViewController:
struct MyView: UIViewControllerRepresentable {
var response: MyResponse
func makeUIViewController(context: Context) -> MyViewController {
return MyViewController(response: response)
}
}
I'm assuming I can pass the response in. However, how do I handle the asynchronousity of it with respect to the body of the view modifier?
struct ShowMyView: ViewModifier {
#Binding var isPresented: Bool
func body(content: Content) -> some View {
content
.sheet(isPresented: $isPresented) {
// something that returns MyView here?
}
.onReceive(Just(isPresented)) { _ in
getMyResponse()
}
}
func getMyResponse() {
getResponse() { result in
switch result {
case .success(let response):
// Something needs to happen here
case .failure:
break
}
}
}
}
I couldn't figure it out. Any help appreciated!

I think this is what you want:
struct ShowMyView: ViewModifier {
#Binding var shouldPresent: Bool
#State private var response: MyResponse? = nil
private var isPresentedBinding: Binding<Bool> {
return Binding(
get: { shouldPresent && response != nil },
set: { shouldPresent = $0 }
)
}
func body(content: Content) -> some View {
content
.sheet(isPresented: isPresentedBinding) {
if let response = response {
MyView(response: response)
}
}
.onChange(of: shouldPresent) {
if $0 && response == nil {
getMyResponse()
}
}
}
func getMyResponse() {
getResponse() { result in
switch result {
case .success(let response):
self.response = response
case .failure:
break
}
}
}
}

You need internal state, like (typed here, so some typos/errors might needed to be fixed)
struct ShowMyView: ViewModifier {
#Binding var isPresented: Bool
#State private var result: MyResponse? = nil
func body(content: Content) -> some View {
content
.sheet(item: $result) { // MyResponse needed to be Identifiable
switch $0 {
case .success(let response):
MyView(response: response)
case .failure:
// something appropriate here, e.g. ErrorView()
break
}
}
.onReceive(Just(isPresented)) { _ in
getMyResponse()
}
}
func getMyResponse() {
getResponse() { result in
DispatchQueue.main.async {
self.result = result // update UI on main queue
}
}
}
}

Related

Use of .fileExporter

i have added a fileExporter to my code so that when the user click the export button, it toggle the value of $showingExporter and expect to show the fileExporter. It works fine, except I found that it will call the method "CreateCSV" in my case no matter whether my button is clicked. The function CreateCSV will be called whenever my View dismiss. Any idea?
var body: someView {
NavigationView {
VStack {
List {
Button(action: {self.showingExporter.toggle()}) {
Label("Export", systemImage: "square.and.arrow.up")
}
}
}.fileExporter(isPresented: $showingExporter, document: createCSV(), contentType: .plainText) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
private func createCSV() -> TextFile {
print("CreateCSV")
}
I found that whenever my view dismiss, it will call createCSV() once.
SwiftUI uses a declarative syntax so whenever your state changes iOS will redraw the views. In your case when the views redraw, your function createCSV will be called because its return value is an argument for the modifier fileExporter.
To fix this use a state variable for your document, pass it as an argument for fileExporter and toggle the value only when the document is ready.
Modify your code to
#State private var showingExporter: Bool = false
#State private var document: TextFile?
var body: some View {
NavigationView {
VStack {
List {
Button(action: {
document = createCSV()
self.showingExporter.toggle()
}) {
Label("Export", systemImage: "square.and.arrow.up")
}
}
}
.fileExporter(isPresented: $showingExporter, document: document, contentType: .plainText) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
private func createCSV() -> TextFile {
print("CreateCSV")
// Return your text file
return TextFile()
}

in SwiftUI how do I determine view to show based on async call

I am trying to control the flow of my app when it first appears based on an async call to see if the user is logged in or not.
If this call is synchronous I can do a simple switch or if else to determine the view to show.
But I don't know how to handle the flow based on an async request. So far this is what I have done but changing the state var does not change the view which is displayed. The following is in the app file #main function:
enum LoginStatus {
case undetermined
case signedIn
case signedOut
}
#main
struct SignUpApp: App {
#State var loginStatus: LoginStatus = .undetermined
public init() {
getLoginStatus()
}
var body: some Scene {
WindowGroup {
switch loginStatus {
case .undetermined:
Text("SigningIn")
case .signedIn:
EnterAppView()
case .signedOut:
LoginView()
}
}
}
func getLoginStatus() {
someAsyncFunctionToGetSignInStatus { result in
DispatchQueue.main.async {
switch result {
case .success:
self.loginStatus = .signedIn
case .failure:
self.loginStatus = .signedOut
}
}
}
}
}
I imagine this is a common task and wondering what the best way to handle it is.
You have the right idea, but generally with async calls, you're probably going to want to move them into an ObservableObject rather than have them in your View code itself, since views are transient in SwiftUI (although the exception may be the top level App like you have).
class SignInManager : ObservableObject {
#Published var loginStatus: LoginStatus = .undetermined
public init() {
getLoginStatus()
}
func getLoginStatus() {
someAsyncFunctionToGetSignInStatus { result in
//may need to use DispatchQueue.main.async {} to make sure you're on the main thread
switch result {
case .success:
self.loginStatus = .signedIn
case .failure:
self.loginStatus = .signedOut
}
}
}
}
#main
struct SignUpApp: App {
#StateObject var signInManager = SignInManager()
var body: some Scene {
WindowGroup {
switch signInManager.loginStatus {
case .undetermined:
Text("SigningIn")
case .signedIn:
EnterAppView()
case .signedOut:
LoginView()
}
}
}
}
enum LoginStatus {
case undetermined
case signedIn
case signedOut
}
#main
struct SignUpApp: App {
#State var loginStatus: LoginStatus = .undetermined
public init() {
getLoginStatus()
}
var body: some Scene {
WindowGroup {
switch loginStatus {
case .undetermined:
Text("SigningIn")
case .signedIn:
EnterAppView()
case .signedOut:
LoginView()
}
}
}
func getLoginStatus() {
someAsyncFunctionToGetSignInStatus { result in
DispatchQueue.main.async {
switch result {
case .success:
self.loginStatus = .signedIn
case .failure:
self.loginStatus = .signedOut
}
}
}
}
}

Initializer `init(:_rowContent:)` requires that `Type` confirm to `Identifiable`

I am following the KMM tutorial and was able to successfully run the app on Android side. Now I would like to test the iOS part. Everything seems to be fine except the compilation error below. I suppose this must be something trivial, but as I have zero experience with iOS/Swift, I am struggling fixing it.
My first attempt was to make RocketLaunchRow extend Identifiable, but then I run into other issues... Would appreciate any help.
xcode version: 12.1
Full source:
import SwiftUI
import shared
func greet() -> String {
return Greeting().greeting()
}
struct RocketLaunchRow: View {
var rocketLaunch: RocketLaunch
var body: some View {
HStack() {
VStack(alignment: .leading, spacing: 10.0) {
Text("Launch name: \(rocketLaunch.missionName)")
Text(launchText).foregroundColor(launchColor)
Text("Launch year: \(String(rocketLaunch.launchYear))")
Text("Launch details: \(rocketLaunch.details ?? "")")
}
Spacer()
}
}
}
extension RocketLaunchRow {
private var launchText: String {
if let isSuccess = rocketLaunch.launchSuccess {
return isSuccess.boolValue ? "Successful" : "Unsuccessful"
} else {
return "No data"
}
}
private var launchColor: Color {
if let isSuccess = rocketLaunch.launchSuccess {
return isSuccess.boolValue ? Color.green : Color.red
} else {
return Color.gray
}
}
}
extension ContentView {
enum LoadableLaunches {
case loading
case result([RocketLaunch])
case error(String)
}
class ViewModel: ObservableObject {
let sdk: SpaceXSDK
#Published var launches = LoadableLaunches.loading
init(sdk: SpaceXSDK) {
self.sdk = sdk
self.loadLaunches(forceReload: false)
}
func loadLaunches(forceReload: Bool) {
self.launches = .loading
sdk.getLaunches(forceReload: forceReload, completionHandler: { launches, error in
if let launches = launches {
self.launches = .result(launches)
} else {
self.launches = .error(error?.localizedDescription ?? "error")
}
})
}
}
}
struct ContentView: View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
NavigationView {
listView()
.navigationBarTitle("SpaceX Launches")
.navigationBarItems(trailing:
Button("Reload") {
self.viewModel.loadLaunches(forceReload: true)
})
}
}
private func listView() -> AnyView {
switch viewModel.launches {
case .loading:
return AnyView(Text("Loading...").multilineTextAlignment(.center))
case .result(let launches):
return AnyView(List(launches) { launch in
RocketLaunchRow(rocketLaunch: launch)
})
case .error(let description):
return AnyView(Text(description).multilineTextAlignment(.center))
}
}
}
using a List or ForEach on primitive types that don’t conform to the Identifiable protocol, such as an array of strings or integers. In this situation, you should use id: .self as the second parameter to your List or ForEach
From the above, we can see that you need to do this on that line where your error occurs:
return AnyView(List(launches, id: \.self) { launch in
I think that should eliminate your error.

Detect keyboard key press events on iOS / SwiftUI [duplicate]

How can I detect keyboard events in a SwiftUI view on macOS?
I want to be able to use key strokes to control items on a particular screen but it's not clear how I detect keyboard events, which is usually done by overriding the keyDown(_ event: NSEvent) in NSView.
New in SwiftUI bundled with Xcode 12 is the commands modifier, which allows us to declare key input with keyboardShortcut view modifier. You then need some way of forwarding the key inputs to your child views. Below is a solution using a Subject, but since it is not a reference type it cannot be passed using environmentObject - which is really what we wanna do, so I've made a small wrapper, conforming to ObservableObject and for conveninece Subject itself (forwarding via the subject).
Using some additional convenience sugar methods, I can just write like this:
.commands {
CommandMenu("Input") {
keyInput(.leftArrow)
keyInput(.rightArrow)
keyInput(.upArrow)
keyInput(.downArrow)
keyInput(.space)
}
}
And forward key inputs to all subviews like this:
.environmentObject(keyInputSubject)
And then a child view, here GameView can listen to the events with onReceive, like so:
struct GameView: View {
#EnvironmentObject private var keyInputSubjectWrapper: KeyInputSubjectWrapper
#StateObject var game: Game
var body: some View {
HStack {
board
info
}.onReceive(keyInputSubjectWrapper) {
game.keyInput($0)
}
}
}
The keyInput method used to declare the keys inside CommandMenu builder is just this:
private extension ItsRainingPolygonsApp {
func keyInput(_ key: KeyEquivalent, modifiers: EventModifiers = .none) -> some View {
keyboardShortcut(key, sender: keyInputSubject, modifiers: modifiers)
}
}
Full Code
extension KeyEquivalent: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.character == rhs.character
}
}
public typealias KeyInputSubject = PassthroughSubject<KeyEquivalent, Never>
public final class KeyInputSubjectWrapper: ObservableObject, Subject {
public func send(_ value: Output) {
objectWillChange.send(value)
}
public func send(completion: Subscribers.Completion<Failure>) {
objectWillChange.send(completion: completion)
}
public func send(subscription: Subscription) {
objectWillChange.send(subscription: subscription)
}
public typealias ObjectWillChangePublisher = KeyInputSubject
public let objectWillChange: ObjectWillChangePublisher
public init(subject: ObjectWillChangePublisher = .init()) {
objectWillChange = subject
}
}
// MARK: Publisher Conformance
public extension KeyInputSubjectWrapper {
typealias Output = KeyInputSubject.Output
typealias Failure = KeyInputSubject.Failure
func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {
objectWillChange.receive(subscriber: subscriber)
}
}
#main
struct ItsRainingPolygonsApp: App {
private let keyInputSubject = KeyInputSubjectWrapper()
var body: some Scene {
WindowGroup {
#if os(macOS)
ContentView()
.frame(idealWidth: .infinity, idealHeight: .infinity)
.onReceive(keyInputSubject) {
print("Key pressed: \($0)")
}
.environmentObject(keyInputSubject)
#else
ContentView()
#endif
}
.commands {
CommandMenu("Input") {
keyInput(.leftArrow)
keyInput(.rightArrow)
keyInput(.upArrow)
keyInput(.downArrow)
keyInput(.space)
}
}
}
}
private extension ItsRainingPolygonsApp {
func keyInput(_ key: KeyEquivalent, modifiers: EventModifiers = .none) -> some View {
keyboardShortcut(key, sender: keyInputSubject, modifiers: modifiers)
}
}
public func keyboardShortcut<Sender, Label>(
_ key: KeyEquivalent,
sender: Sender,
modifiers: EventModifiers = .none,
#ViewBuilder label: () -> Label
) -> some View where Label: View, Sender: Subject, Sender.Output == KeyEquivalent {
Button(action: { sender.send(key) }, label: label)
.keyboardShortcut(key, modifiers: modifiers)
}
public func keyboardShortcut<Sender>(
_ key: KeyEquivalent,
sender: Sender,
modifiers: EventModifiers = .none
) -> some View where Sender: Subject, Sender.Output == KeyEquivalent {
guard let nameFromKey = key.name else {
return AnyView(EmptyView())
}
return AnyView(keyboardShortcut(key, sender: sender, modifiers: modifiers) {
Text("\(nameFromKey)")
})
}
extension KeyEquivalent {
var lowerCaseName: String? {
switch self {
case .space: return "space"
case .clear: return "clear"
case .delete: return "delete"
case .deleteForward: return "delete forward"
case .downArrow: return "down arrow"
case .end: return "end"
case .escape: return "escape"
case .home: return "home"
case .leftArrow: return "left arrow"
case .pageDown: return "page down"
case .pageUp: return "page up"
case .return: return "return"
case .rightArrow: return "right arrow"
case .space: return "space"
case .tab: return "tab"
case .upArrow: return "up arrow"
default: return nil
}
}
var name: String? {
lowerCaseName?.capitalizingFirstLetter()
}
}
public extension EventModifiers {
static let none = Self()
}
extension String {
func capitalizingFirstLetter() -> String {
return prefix(1).uppercased() + self.lowercased().dropFirst()
}
mutating func capitalizeFirstLetter() {
self = self.capitalizingFirstLetter()
}
}
extension KeyEquivalent: CustomStringConvertible {
public var description: String {
name ?? "\(character)"
}
}
There is no built-in native SwiftUI API for this, so far.
Here is just a demo of a possible approach. Tested with Xcode 11.4 / macOS 10.15.4
struct KeyEventHandling: NSViewRepresentable {
class KeyView: NSView {
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
print(">> key \(event.charactersIgnoringModifiers ?? "")")
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
DispatchQueue.main.async { // wait till next event cycle
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
struct TestKeyboardEventHandling: View {
var body: some View {
Text("Hello, World!")
.background(KeyEventHandling())
}
}
Output:
There's another solution that is very simple but only works for particular types of keys - you'd have to experiment. Just create Buttons with .keyboardShortcut modifier, but hide them visually.
Group {
Button(action: { goAway() }) {}
.keyboardShortcut(.escape, modifiers: [])
Button(action: { goLeft() }) {}
.keyboardShortcut(.upArrow, modifiers: [])
Button(action: { goDown() }) {}
.keyboardShortcut(.downArrow, modifiers: [])
}.opacity(0)

How to present a view after a request with URLSession in SwiftUI?

I want to present a view after I receive the data from a request, something like this
var body: some View {
VStack {
Text("Company ID")
TextField($companyID).textFieldStyle(.roundedBorder)
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
self.presentation(Modal(LogonView(), onDismiss: {
print("dismiss")
}))
}
}.resume()
}
}
Business logic mixed with UI code is a recipe for trouble.
You can create a model object as a #ObjectBinding as follows.
class Model: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var shouldPresentModal = false {
didSet {
didChange.send(())
}
}
func fetch() {
// Request goes here
// Edit `shouldPresentModel` accordingly
}
}
And the view could be something like...
struct ContentView : View {
#ObjectBinding var model: Model
#State var companyID: String = ""
var body: some View {
VStack {
Text("Company ID")
TextField($companyID).textFieldStyle(.roundedBorder)
if (model.shouldPresentModal) {
// presentation logic goes here
}
}.onAppear {
self.model.fetch()
}
}
}
The way it works:
When the VStack appears, the model fetch function is called and executed
When the request succeeds shouldPresentModal is set to true, and a message is sent down the PassthroughSubject
The view, which is a subscriber of that subject, knows the model has changed and triggers a redraw.
If shouldPresentModal was set to true, additional UI drawing is executed.
I recommend watching this excellent WWDC 2019 talk:
Data Flow Through Swift UI
It makes all of the above clear.
I think you can do smth like that:
var body: some View {
VStack {
Text("Company ID")
}
.onAppear() {
self.loadContent()
}
}
private func loadContent() {
let url = URL(string: "https://your.url")!
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
self.presentation(Modal(ContentView(), onDismiss: {
print("dismiss")
}))
}
}.resume()
}

Resources