Networking in SwiftUI
Tags: ios, swift, swiftui
Published: 2020-02-11 07:53:17

---
title: Networking in SwiftUI
published: true
description: 
tags: ios, swift, swiftui
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/088mzmw0g0i806pcwqd0.png
series: CometChat SwiftUI course
canonical_url: https://www.cometchat.com/tutorials/networking-in-swiftui-7-7/
---

By now, you did all there is do with your app's UI. You have a way to log in, show contacts and display incoming messages. But one crucial bit is missing: Your app is not connected to anything.

In this final part of the SwiftUI course, you'll breathe life into your app by adding networking to send and receive messages to a web server!

To help you, you'll use CometChat Pro. CometChat Pro is a cross-platform chat SDK that lets you build chat apps that work on iOS, Android and the web at the same time. It takes care of storing, receiving and sending messages to your users' devices. There's no need to deal with servers, backend code or WebSockets.

> You can find a link to the [finished project code on GitHub](https://github.com/cometchat-pro-tutorials/swift-ui-chat).

## Installing CometChat

Before you can use CometChat, you'll first need to install the SDK as a dependency. To do this, you'll use CocoaPods, a dependency manager for iOS. 

> **Note:** If you don't have CocoaPods installed, enter the following command in **Terminal**:
> 
> ```bash
> sudo gem install cocoapods
> ```

Open **Terminal** and, using **cd**, navigate to your project's root folder. (The one that contains the **.xcodeproj** file.)

Enter the following command:

```bash
pod init
```

This will initialize CocoaPods for your project and create a new file called **Podfile**. Open this file in your favorite text editor, and update its contents to the following:

```ruby
target 'CometChat' do
  use_frameworks!
  pod 'CometChatPro', '~> 2.0.5'
end
```

Save the file and head back to **Terminal** to enter the following command:

```bash
pod install
```

This will install CometChat into your app, and create a new file with the **.xcworkspace** extension. Close any Xcode projects you might have active, and open the **.xcworkspace** file. From this point on, you'll use the workspace, instead of the project file, to develop your app.

## Setting up

Now that CometChat is in the app, let's begin using it. First, you'll create a CometChat account — don't worry, it's free!

Head over to [CometChat's registration page](https://app.cometchat.io/register) and sign up. Once you sign up, you'll get taken to a dashboard. Create a new app under **Add New App**. Select **USA** as the region and enter **CometChat** as the app's name. Click the **+** button to create the app.

![Creating a CometChat account](https://dev-to-uploads.s3.amazonaws.com/i/4jv6n8169fnvqppcwz5e.png)

After 15 or so seconds, you'll have an up-and-running chat service online! All you need to do is connect to it.

Open your new app's dashboard by clicking **Explore**. Head over to **API Keys** in the sidebar. Take a look at the keys names **API Key with full access scope**. Note down the **API Key** and **App ID** — you'll need these in a second.

![Your CometChat API key and App ID](https://dev-to-uploads.s3.amazonaws.com/i/3cfwenyzp4thzzitvn4s.png)

Back in **Xcode**, create a new plain Swift file named **ChatService.swift**. This file will be the bridge between CometChat's SDK and your app. All SDK-related code will go in this file.

Add the following contents to the file:

```swift
import Foundation
import Combine
import CometChatPro

extension String: Error {}
```

Since we're working with cool new tools like SwiftUI, it makes sense to also build `ChatService` using Combine. So, you'll import Combine as well as CometChat's SDK.

Next, create the `ChatService` class:

```swift
class ChatService {
  
  private enum Constants {
    #warning("Don't forget to set your API key and app ID here!")
    static let cometChatAPIKey = "API_KEY"
    static let cometChatAppID = "APP_ID"
  }
  
}
```

You declare this class and an enum to hold some necessary constants. These will be your CometChat app ID and API key. Make sure to replace the placeholders with the values you saw earlier in the online dashboard.

Next, add a static function to the class that will initialize CometChat:

```swift
static func initialize() {
  let settings = AppSettings.AppSettingsBuilder()
    .subscribePresenceForAllUsers()
    .setRegion(region: "us")
    .build()
  
  _ = CometChat(
    appId: Constants.cometChatAppID,
    appSettings: settings,
    onSuccess: { isSuccess in
      print("CometChat connected successfully: \(isSuccess)")
    }, onError: { error in
      print(error)
    })
}
```

This sets up CometChat so that it works with your app. Make sure that the `region` matches the one you set in the online dashboard (`us` and `eu`, respectively).

You'll call this function whenever your app launches. The perfect place for this is in **SceneDelegate.swift**. Add the following line to the top of `scene`:

```swift
ChatService.initialize()
```

Now CometChat will get initialized when a user starts your app.

There's one more step to finish your setup. To use `ChatService`, your views need a way to access it. Thankfully, you already created an object that you add to your root view's environment — `AppStore`.

Head to **AppStore.swift** and add a new property to the nested `AppState` struct:

```swift
struct AppState {
  var currentUser: Contact?
  let chatService: ChatService
}
```

Next, modify the declaration of `state` to include `ChatService`:

```swift
@Published private(set) var state = AppState(
  currentUser: nil, 
  chatService: ChatService())
```

Now, any view can fetch the `ChatService` by getting it from the `AppStore` environment object. Convenient!

Run the app and check the console. You should see a message saying that you connected to CometChat. It lives!

## Logging in

With our setup done, it's time to move onto some real features. We'll start with logging in. You already implemented the UI for this, all that's left is telling CometChat a user wants to log in.

Open up **ChatService.swift** and add a new property:

```swift
private var user: Contact?
```

This property will store the currently logged in user. Because there's one shared `ChatService` instance in the environment, all views will have access to the same user.

CometChat has its own `User` class, but you won't be using this in your app. Instead, you'll add a way to convert CometChat's `User`s to `Contact`s.

Open **Contact.swift** and import CometChat at the top of the file:

```swift
import CometChatPro
``` 

Next, add the following extension to the bottom of the file:

```swift
extension Contact {
  init(_ cometChatUser: User) {
    self.id = cometChatUser.uid ?? "unknown"
    self.name = cometChatUser.name ?? "unknown"
    self.avatar = cometChatUser.avatar.flatMap(URL.init)
    self.isOnline = cometChatUser.status == .online
  }
}
```

This initializes a `Contact` with a CometChat `User` instance.

### Combine's Futures and Promises

Next, get back to **ChatService.swift** and add a method to log the user in:

```
func login(email: String) -> Future<Contact, Error> {

}
```

You might be wondering, what the hell is a `Future`? If you're coming from React or a similar JavaScript-based framework, you might be familiar with a thing called promises. Promises and futures are terms that are used in different ways in different languages, but they generally achieve the same thing: Replacing callbacks with a more sane programming model.

`Future` is a Combine publisher that will _eventually_ produce a single value. For instance, if you're making a network request to fetch a user, you can represent that value as `Future<User, Error>`. _Right now_, there's no value, but in the future, there'll be a user *or* an error.

Put another way, a `Future` _promises_ a value. That's why promises and futures are often used interchangeably.

Futures are usually a replacement for _callbacks_. Before Combine, each network request had a callback function that it would call when the request completes. Instead of returning a value, the functions would _receive_ a callback.

Futures allow you to _return_ a value (a future) right away — even if the data isn't yet loaded. This gives the caller more flexibility. They can get the future and do whatever they want with it: Attach callbacks, perform transformations with `map` or `compactMap`, cancel the request mid-way through, ignore the result and so on.

I think it's safe to say futures are *the future* of asynchronous code in Swift.

### Creating a Future

Let's get back to `login`. Since `login` returns a `Future`, you'll have to create one in the function. Add the following to `login`:

```swift
return Future<Contact, Error> { promise in

}
```

Here, you'll immediately return a `Future` from the function. A `Future` is initialized with a closure. This closure has one argument: A function called a _promise_. The promise is a function that you'll call when you get the data you want to load.

To sum up: `Future` is a struct that gets initialized with a _closure_. This closure is a function that takes _another_ function as its argument called a _promise_. Within the closure, you'll call this promise to push data through the promise to the caller.

It may sound complex, but after a few goes at creating futures, you'll get the hang of it!

Add the code to `login` inside `Future`'s closure:

```swift
// 1
CometChat.login(
  UID: email,
  apiKey: Constants.cometChatAPIKey,
  onSuccess: { [weak self] cometChatUser in
    
    // 2
    guard let self = self else { return }
    self.user = Contact(cometChatUser)
    DispatchQueue.main.async {
      // 3
      promise(.success(self.user!))
    }
  },
  onError: { error in
    
    // 4
    print("Error logging in:")
    print(error.errorDescription)
    DispatchQueue.main.async {
      promise(.failure("Error logging in"))
    }
  })
```

Here's what's going on in the closure:

1. You call CometChat's `login` function and pass it the user's unique ID (email) and the app's API key.
2. If CometChat manages to log the user in, you'll convert the user to a `Contact` and set the property you created earlier to store the user.
3. After dispatching back to the main queue, you'll call `promise` with a successful value. This notifies anyone listening to the `Future` that new data has arrived.
4. On the other hand, if something goes wrong, you'll call `promise` with an unsuccessful value, giving anyone listening an error.

### Using Futures

Now you can hook up your login UI with the function you just created. Open up **LoginView.swift** and add an import to the top of the file:

```swift
import Combine
```

Next, add the following property to the struct:

```swift
@State private var subscriptions: Set<AnyCancellable> = []
```

You've already learned about cancellables. Here, you declare a set to hold all of `LoginView`'s subscriptions. Since `LoginView` is a `struct` (and thus immutable), you'll need to mark the set as `@State` before you can add new items to the set.

Next, modify `login` so that it calls the function you created in `ChatService`:

```swift
func login() {
  store.state.chatService.login(email: email)
    .sink(receiveCompletion: { error in
      print(error)
    }, receiveValue: { user in
      self.store.setCurrentUser(user)
      self.showContacts = true
    })
    .store(in: &subscriptions)
}
```

`login` returns a `Future`, so you call that future's `sink` method. `sink` is not specific to futures, though. It's the way you attach _closures_ to Combine `Publisher`s. Whenever the publisher publishes a new event, the closure you provide in `sink` gets called.

In this case, you provide two closures: One for a failed, and one for a successful result. On failure, you'll print out the error. If the user is logged in successfully, however, you'll set the current user on the `AppStore` and initiate a transition to the contacts screen. Finally, you'll store the cancellable inside the set you added to the struct.

Build and run the app and try to log in. If you try to use your email you'll get an error. This is expected — your email doesn't exist in the database of users! Thankfully, there are some values you can use to test out your chat app.

CometChat gives you five default users with user IDs **superhero1** through **superhero5**. You can see these users if you navigate to the **Users** page on CometChat's online dashboard.

Enter **superhero1** as the email. This is Iron Man's top-secret username, so don't share it with anyone!

![Logging in with CometChat in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/htt259ym0x6kyxhzz42o.gif)

If everything goes correctly, you'll get sent to the contacts screen. This means your app logged in to CometChat! You're successfully communicating with an online chat service.

The contacts you see are still fake, though. Let's take care of that.

## Fetching Contacts from CometChat

CometChat stores each registered contact for you. This means you can easily fetch all existing users to display them on the contacts screen.

Head back to **ChatService.swift** and add the following code to the class:

```swift
private var usersRequest: UsersRequest?
func getUsers() -> Future<[Contact], Error> {
  usersRequest = UsersRequest.UsersRequestBuilder().build()
  
}
```

You create a `UsersRequest` and store a reference to it. You do this using a `UsersRequestBuilder`: It allows you to configure your request in different ways. For instance, you can only fetch the user's friends, hide blocked users, paginate the request and so on.

Next, you'll create another future and use the request to fetch all users in your app:

```swift
return Future<[Contact], Error> { promise in
  self.usersRequest?.fetchNext(
    onSuccess: { cometChatUsers in
      let users = cometChatUsers.map(Contact.init)
      DispatchQueue.main.async {
        promise(.success(users))
      }
    },
    onError: { error in
      let message = error?.errorDescription ?? "unknown"
      promise(.failure(message))
      print("Fetching users failed with error:")
      print(message)
    })
}
```

If the request returns successfully, you'll convert each user to a `Contact` and pass all the users to the caller. Otherwise, if something goes wrong, you'll print out an error.

Let's call this function from **ContactsView.swift**. First, import Combine in the file:

```swift
import Combine
```

Next, add a set of cancellables to the struct:

```swift
@State private var subscriptions: Set<AnyCancellable> = []
```

Then, add the following function:

```swift
private func getContacts() {
  store.state.chatService.getUsers()
    .sink(receiveCompletion: { error in
      print(error)
    }, receiveValue: { contacts in
      self.items = contacts.map {
        ContactRow.ContactItem(
          contact: $0,
          lastMessage: "",
          unread: false)
      }
    })
    .store(in: &subscriptions)
}
```

Here, you use `sink` to respond to the future as you did earlier. In this case, when you get users you'll set them into your state variable, letting the view update itself. If you get an error, you'll print it to the console.

Next, call this method when the view appears by adding the following line to the bottom of `body`:

```swift
.onAppear(perform: getContacts)
```

Finally, change the declaration of `items` to remove hard-coded contacts:

```swift
@State private var items: [ContactRow.ContactItem] = []
```

You won't be needing these anymore. Run the app, log in as **superhero1** and move on to the contacts screen. Instead of seeing hard-coded contacts, you'll see the users CometChat created for you, including Captain America, Spiderman and even a few of the X-Men!

![Populating a SwiftUI list from the network](https://dev-to-uploads.s3.amazonaws.com/i/v2lg1iposgujtlevxsvw.png)

### Updating a contact's online status in real-time

Currently, all of your contacts are showing as offline. There are two reasons for that: One, nobody else is using your app yet. But, more importantly, you also haven't implemented a way to track their online status. CometChat tracks this and notifies you when they come online. You'll use that to update your views.

### Converting delegates into Combine publishers with passthrough subjects

CometChat uses the delegate pattern to notify you of changes to a contact's online status. This is a useful pattern that is used universally in UIKit. In SwiftUI, we can take advantage of Combine and its declarative nature for code that is more straightforward than delegates. To do this, we'll use Combine's `Subject`s to convert the delegate methods into Combine publishers.

Back in **ChatService.swift**, add the following two properties to the top of the class:

```swift
private let userStatusChangedSubject = PassthroughSubject<Contact, Never>()
let userStatusChanged: AnyPublisher<Contact, Never>
```

You just declared your first subject! Subjects are a special kind of Combine publisher that is a bridge between **imperative** and **reactive** programming. Subjects let you manually send events to their subscribers. This is useful for wrapping code that doesn't support Combine into a neat Combine wrapper.

Next, add an initializer to the class:

```swift
init() {
  userStatusChanged = userStatusChangedSubject.eraseToAnyPublisher()
}
```

Notice that you declared a _private_ subject and a _public_ `AnyPublisher`. Since anyone can add their events to a subject, it's good practice to make it private. The same way you would make setters private and getters public for regular variables. By calling `eraseToAnyPublisher`, you get a version of the subject that is immutable.

Next, add the following line to the top of `login`, just before the `return` statement:

```swift
CometChat.userdelegate = self
```

You register as CometChat's user delegate so that it notifies you when a user's status changes.

There's one final bit to add to `ChatService`. Add the following extension that implements `CometChatUserDelegate` to the bottom of the file:

```swift
extension ChatService: CometChatUserDelegate {
  
  func onUserOnline(user cometChatUser: CometChatPro.User) {
    DispatchQueue.main.async {
      self.userStatusChangedSubject.send(Contact(cometChatUser))
    }
  }
  
  func onUserOffline(user cometChatUser: CometChatPro.User) {
    DispatchQueue.main.async {
      self.userStatusChangedSubject.send(Contact(cometChatUser))
    }
  }
}
```

Here, you use the subject's `send` method to send the updated contact through the subject. Anyone listening to the subject (or the erased version) will receive this contact.

Head back to **ContactsView.swift** to connect it to the code you just wrote. First, add the following method:

```swift
private func updateContactsOnChange() {
  store.state.chatService.userStatusChanged.sink {
    newContact in
    
    guard let index = self.items.firstIndex(
      where: { $0.contact.id == newContact.id }) else {
      return
    }
    
    self.items[index] = ContactRow.ContactItem(
      contact: newContact,
      lastMessage: "",
      unread: false)
  }.store(in: &subscriptions)
}
```

This method adds a `sink` to the publisher you exposed earlier. When an updated contact appears, you find that contact's index and update it. Because `items` is a state variable, the view will automatically update.

Finally, call this function from the end of `body`:

```swift
.onAppear(perform: updateContactsOnChange)
```

Build and run the app and log in as **superhero1**. All contacts will still be offline because nobody else is using your app yet. You can open another Simulator instance and log in as **superhero2**.

![Live updating online status in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/ytp9f0f09jtdxn34kb7v.png)

You'll see the user's online badge turn green. When you exit the app, it will turn grey after a while. Neat!

## Messages

Okay, that was enough preamble. It's time to get to the bread and butter of your app: **Sending and receiving messages.** Thankfully, since you are now fetching contacts as well as listening to contact changes, implementing support for messages will be similar. You'll use the same patterns you used for contacts: A request to fetch existing messages and a subject for when new messages arrive in real-time.

First, you'll add a way to convert CometChat's `TextMessage` into your own `Message`. Open **Message.swift** and import CometChat into the file:

```swift
import CometChatPro
```

Next, add the following extension to the bottom of the file:

```swift
extension Message {
  init?(_ message: CometChatPro.BaseMessage) {
    guard let message = message as? TextMessage,
      let sender = message.sender else {
      return nil
    }
    
    self.id = message.id
    self.text = message.text
    self.contact = Contact(sender)
  }
}
```

This is an optional initializer that tries to create a `Message` based on CometChat's `TextMessage`.

### Receiving messages

You'll start by receiving messages from CometChat. The same way you did for your contacts, add two new properties to track new messages:

```swift
private let receivedMessageSubject = PassthroughSubject<Message, Never>()
let receivedMessage: AnyPublisher<Message, Never>
```

Next, erase the subject at the end of `init`:

```swift
receivedMessage = receivedMessageSubject.eraseToAnyPublisher()
```

You need to set `ChatService` as CometChat's message delegate. Do so by adding a new line at the top of `login`:

```swift
CometChat.messagedelegate = self
```

Next, implement this delegate in an extension at the bottom of the file:

```swift
extension ChatService: CometChatMessageDelegate {
  func onTextMessageReceived(textMessage: TextMessage) {
    DispatchQueue.main.async {
      guard let message = Message(textMessage) else {
        return
      }
      
      self.receivedMessageSubject.send(message)
    }
  }
}
```

When you get a new message, you'll convert it to your own `Message` struct and send it through the subject.

Now, open **ChatView.swift** to add code that listens for new messages. First, add an import to the top of the file:

```swift
import Combine
```

Then, fetch `AppStore` from the environment so you get access to the chat service:

```swift
@EnvironmentObject private var store: AppStore
```

Next, add a set of cancellables to the struct:

```swift
@State private var subscriptions: Set<AnyCancellable> = []
```

With those pieces in place, you can create a new function that listens for new messages:

```swift
private func updateMessagesOnChange() {
  store.state.chatService.receivedMessage.sink { message in
    guard message.contact.id == self.receiver.id ||
      message.contact.id == self.currentUser.id else {
      return
    }
    
    self.messages.append(message)
  }.store(in: &subscriptions)
}
```

You add a `sink` to the messages publisher. First, you check that the message is relevant for this view: The message has to either come from the logged-in user, or this screen's receiver. This prevents messages from other users from showing up on this screen. If the message is relevant, you'll add it to the list of messages.

Call this method when the view appears by adding the following line to the bottom of `body`:

```swift
.onAppear(perform: updateMessagesOnChange)
```

Finally, remove all hard-coded messages from the declaration of `messages`:

```swift
@State private var messages: [Message] = []
```

You can run the project now to check everything is working, but you have no way of testing receiving messages: You can't send them yet! Let's fix that.

### Sending messages

Now that you can _receive_ messages, it's time to start sending them!

Back in **ChatService.swift**, add a new function to the class:

```swift
func send(message: String, to reciever: Contact) {
  guard let user = user else {
    return
  }
  
  let textMessage = TextMessage(
    receiverUid: reciever.id,
    text: message,
    receiverType: .user)
    
}
```

The `send(message:to:)` function will, naturally, send your messages. It receives the message you'll send as well as the user you're sending it to. In the function, you'll construct a new `TextMessage` with the necessary information. This is a class from CometChat. Besides text messages, CometChat also supports media messages and even custom messages where you can pass any data you like.

Next, add the following code to the function to send the message:

```swift
CometChat.sendTextMessage(
  message: textMessage,
  onSuccess: { [weak self] sentMessage in
    DispatchQueue.main.async {
      self?.receivedMessageSubject.send(Message(
        id: sentMessage.id,
        text: message,
        contact: user))
    }
  },
  onError: { error in
    print("Error sending message:")
    print(error?.errorDescription ?? "")
})
```

You send the message by calling CometChat's `sendTextMessage`. If it's sent successfully, you'll add the message to your subject. The view will respond to the message by adding it to the list of messages.

_Now_ you can test this. Run the app and log in as **superhero1**. When you reach the contacts screen, start chatting with **Captain America** (superhero2). Launch another simulator instance and, this time, log in as **superhero2**. Open **Iron Man** and chat away!

![Live updating the SwiftUI chat app with new messages](https://dev-to-uploads.s3.amazonaws.com/i/py8o6skexs2bmyyjjr3f.png)

The fact that you're chatting with *yourself* doesn't make your app any less impressive: You now have a functioning chat app that lets you communicate with another person over a web server! Congrats!

> **Note**: You might notice that, after you get a few messages, new messages appear off-screen. This can be solved by scrolling to the last message whenever a new one appears. In pure SwiftUI, that's easier said than done! Currently, there's no API to manipulate the scroll position of a scroll view. The best way to do this is to wrap `UIScrollView` in a `UIViewControllerRepresentable`. This allows SwiftUI to use UIKit's `UIScrollView`. Check out [Apple's official tutorial on how to do this](https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit).

### Loading past messages

There's only one final thing to add to your app. If you reset the app, all the messages that you sent and received are gone! Don't worry, they're still saved on CometChat — you just need to fetch them.

Back in **ChatService.swift**, add one final method to the class:

```swift
private var messagesRequest: MessagesRequest?
func getMessages(from sender: Contact) -> Future<[Message], Error> {
  let limit = 50
  
  messagesRequest = MessagesRequest.MessageRequestBuilder()
    .set(limit: limit)
    .set(uid: sender.id)
    .build()
  
}
```

Similarly to how you fetched the contacts, you'll create a message request using a builder. In this case, you'll fetch the last 50 messages sent to and from a specific user.

Next, add the following code to the method:

```swift
return Future<[Message], Error> { promise in
  self.messagesRequest!.fetchPrevious(
    onSuccess: { fetchedMessages in
      let messages = (fetchedMessages ?? [])
        .compactMap(Message.init)
      
      DispatchQueue.main.async {
        promise(.success(messages))
      }
    },
    onError: { error in
      let message = error?.errorDescription ?? "unknown"
      print("Fetching messages failed with error:")
      print(message)
      promise(.failure(message))
    })
}
```

You create a future like before. When you successfully get the messages, you'll send them through the future by calling `promise`. If something goes wrong, however, you'll send an error.

Now, open **ChatView.swift** and add a new method:

```swift
private func getMessages() {
  store.state.chatService.getMessages(from: receiver)
    .sink(receiveCompletion: { error in
      print(error)
    }, receiveValue: { messages in
      self.messages = messages
    })
    .store(in: &subscriptions)
}
```

You call the function to get the messages and, on success, update the state variable. On failure, you'll print out an error.

Finally, call this method when the view appears at the bottom of `body`:

```swift
.onAppear(perform: getMessages)
```

Run the app one final time, login in as **superhero1** and start chatting with **Captain America**. You'll see all of your past messages load.

![Loading previous messages in the SwiftUI chat app](https://dev-to-uploads.s3.amazonaws.com/i/qwh4765pvy91yb3ma9ib.png)

## Conclusion

After 7 parts of this SwiftUI course and, really not that much code, you made a fully working chat app!

Throughout this course you've learned:

 - How to create, compose and layout SwiftUI views.
 - How to think in a SwiftUI mindset.
 - How to manage state in SwiftUI.
 - How to thread data through your app using Combine.
 - How to use the Environment.
 - How to initiate network calls and return Futures.

 This is more than you need to start going off on your own and discovering SwiftUI for yourself. But, if you want some more in-depth knowledge, here's some reading material:
 
 - [Apple's WWDC 2019 sessions on SwiftUI](https://developer.apple.com/videos/wwdc2019/?q=swiftui) are great learning resources in video form. However, some information may be a little outdated now.
 - [SwiftUI by Tutorials](https://store.raywenderlich.com/products/swiftui-by-tutorials), RayWenderlich.com's book on SwiftUI provides some more in-depth knowledge on SwiftUI and might give a different viewpoint than I did.
 - Additionally, there's also [a bundle](https://store.raywenderlich.com/products/advanced-ios-summer-bundle-2019) of SwiftUI, Combine and Catalyst books, so you can learn everything new and shiny in one go.
 - [SwiftUI by Example](https://www.hackingwithswift.com/quick-start/swiftui) is a _free_ online book by Paul Hudson, though might be less in-depth than other resources.

SwiftUI is still fairly young so the biggest learning resource you have is **your head**. Go out, experiment, challenge yourself and figure stuff out! When you do, don't forget to write about it! (Or at least answer StackOverflow questions. :])

Good luck and have fun on your SwiftUI journeys! If you have any questions, feel free to comment below or message me on Twitter.