iPadOS Multitasking: Using Multiple Windows for Your App
In this iPadOS Multitasking tutorial, you’ll learn how to get the most out of iPad screens and multitasking features for your app. By David Piper.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iPadOS Multitasking: Using Multiple Windows for Your App
25 mins
Supporting Drag and Drop in SwiftUI
Go to HeroRow.swift and add the following code right below Group
in body
:
.onDrag { () -> NSItemProvider in
return NSItemProvider(object: self.hero)
}
When dragging a row, this modifier takes a closure returning an NSItemProvider
. Since the hero of the dragged row conforms to NSItemProviderWriting
, you can call the initializer of NSItemProvider
.
Now, you’ll update the list of favorite heroes. Open FavoriteList.swift. Add this method right below body
in FavoriteList
:
func addFavoriteHero(from itemProvider: [NSItemProvider]) {
// 1
for provider in itemProvider {
guard provider.canLoadObject(ofClass: MarvelCharacter.self) else {
continue
}
// 2
_ = provider.loadObject(ofClass: MarvelCharacter.self) { hero, _ in
// 3
guard
let hero = hero as? MarvelCharacter,
!self.state.favorites.contains(where: { $0.name == hero.name })
else { return }
// 4
DispatchQueue.main.async {
self.state.favorites.append(hero)
self.state.favorites.sort {
return $0.name < $1.name
}
}
}
}
}
Here's a breakdown:
- To ensure you can handle the received providers, you loop over them and call
canLoadObject(ofClass:)
. If this method returnsfalse
, continue with the next provider. - Call
loadObject(ofClass:)
for each provider to decode the wrapped data back to a hero. - In the closure, check that the given hero isn't already a favorite before adding it to the list. Otherwise, he'll show up more than once in the favorites list.
- Finally, sort the favorite heroes by name, so the order is the same as in the overview list. Notice, you used
DispatchQueue.main.async
so the code executes in the main thread asynchronously.
Now it's time to add dropping. At the top of the file, add the following import statement:
import UniformTypeIdentifiers
In FavoriteList
, replace body
with the following code:
var body: some View {
VStack {
ZStack {
Circle()
.fill(Color("rw-green"))
.frame(width: 200.0, height: 200.0)
.onDrop(of: [UTType.data.identifier], isTargeted: nil) { provider in
self.addFavoriteHero(from: provider)
return true
}
Text("Drop here!")
.foregroundColor(.white)
}
List {
ForEach(state.favorites) { hero in
HeroRow(hero: hero, state: self.state)
}
}
}
}
This code adds a circle with the raywenderlich.com color scheme right above the list and a short text. The text tells the user this is the right place to drop a hero.
This time, onDrop
takes a list of UTIs defining types the modifier can handle. It also takes a closure to handle the received NSItemProvider
wrapping the dropped items.
Build and run. Open two instances: One with all heroes and one with the favorites. Drag one item from the list to the green Drop Here! circle in favorites.
The app will look like this:
Place your finger above the dropping area, and a green circle with a plus symbol appears in the corner of the row. It indicates you can drop the hero here.
Notice your app is still interactive while you perform a drag or drop operation, as is standard for all apps.
You can add items to a drag interaction by tapping more heroes while still holding the dragged items. Because it's possible to drop all heroes at once, you need to handle an array of NSItemProvider
in onDrop
above.
But it's a little strange to drop the hero on the circle if the list is right below it. Wouldn't it be better if you could drop the hero right on the list? Try to do that and see the app crashes.
At the moment with SwiftUI, an empty list can't handle drops, but there's a way around this problem.
Handling Drops on Lists
You'll use a trick to make this work. The app won't crash when dropping on a non-empty cell. So, by adding cells with empty content, you can drop a hero on the list.
Open MarvelCharacter.swift. Add the following code to MarvelCharacter
:
// 1
var isEmptyHero: Bool = false
// 2
static var emptyHero: MarvelCharacter {
let hero = MarvelCharacter(name: "", description: "")
hero.thumbnail = MarvelThumbnail(path: "", extension: "")
hero.isEmptyHero = true
hero.downloadedImage = CodableImage(image: UIImage())
return hero
}
Here's the code breakdown:
- This adds a boolean property
isEmpytyHero
. - Here you add
emptyHero
to provide a hero without name, description, stats or image. By presenting this hero in the favorite cell, it looks like it's empty. But since there's a hero within the cell, the list accepts drops.
Next, open MarvelousHeroesState.swift. In MarvelousHeroesState
, replace favorites
with:
@Published var favorites: [MarvelCharacter] = [
.emptyHero,
.emptyHero,
.emptyHero
]
This code adds some empty heroes as the initial state of the favorites
list. Each row of this list will seem empty, so the whole list appears to have no heroes.
Open FavoriteList.swift. Right below ZStack
, replace List
with the following code:
List {
ForEach(state.favorites) { hero in
HeroRow(hero: hero, state: self.state)
}
.onInsert(of: [UTType.data.identifier]) { _, provider in
self.addFavoriteHero(from: provider)
}
}
This onInsert(of:perform:)
takes a list of the UTIs it can handle. You also need to define a closure to process the dropped NSItemProvider
. Use addFavoriteHero(from:)
as before in the onDrop
modifier.
Finally, handle the empty heroes in the favorite list by sorting them below all the non empty heroes. Open Extensions.swift and add following code at the end of the file:
extension Array where Element == MarvelCharacter {
mutating func appendSorted(_ newElement: Element) {
append(newElement)
sort {
if $0.isEmptyHero { return false }
if $1.isEmptyHero { return true }
return $0.name < $1.name
}
}
}
This extension adds a method to all Array
s consisting of MarvelCharacter
. It appends the new hero and sorts all empty heroes at the end of the array.
There are two places heroes are added to favorites
. Open FavoriteList.swift. In addFavoriteHero(from:)
, replace following code:
DispatchQueue.main.async {
self.state.favorites.append(hero)
self.state.favorites.sort {
return $0.name < $1.name
}
}
With the following:
DispatchQueue.main.async {
self.state.favorites.appendSorted(hero)
}
This adds the hero and sorts favorites
.
Finally, open FavoriteButton.swift and replace addHeroToFavorites()
with:
private func addHeroToFavorites() {
state.favorites.appendSorted(hero)
}
Also, add following code to the beginning of body
:
guard !hero.isEmptyHero else {
return Button(action: { return }, label: { Text("") })
}
This shows a Button
without any text and action for a hero where isEmptyHero
is true
.
Build and run. Drop a hero on the favorite list. This time, the app doesn't crash.
Congratulations, now you can drop heroes all over FavoriteList
. :]