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

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.

Related

SwiftUI LiveActivities on start throw weird errors

So, I trying to work with ActivityKit, to create a simple live activity.
Code:
TimerAttributes.swift
import ActivityKit
import SwiftUI
struct TimerAttributes: ActivityAttributes {
public typealias TimerStatus = ContentState
public struct ContentState: Codable, Hashable {
var endTime: Date
}
var timerName: String
}
Widget
import ActivityKit
import WidgetKit
import SwiftUI
struct TimerActivityView: View {
let context: ActivityViewContext<TimerAttributes>
var body: some View {
VStack {
Text(context.attributes.timerName)
.font(.headline)
Text(context.state.endTime, style: .timer)
}
.padding(.horizontal)
}
}
#main
struct Tutorial_Widget: Widget {
let kind: String = "Tutorial_Widget"
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerAttributes.self) { context in
TimerActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
// 2
}
DynamicIslandExpandedRegion(.trailing) {
// 2
}
DynamicIslandExpandedRegion(.center) {
// 2
}
DynamicIslandExpandedRegion(.bottom) {
// 2
}
} compactLeading: {
// 3
} compactTrailing: {
// 3
} minimal: {
// 4
}
}
}
}
MainView.swift
import ActivityKit
import SwiftUI
struct MainView{
#State private var activity: Activity<TimerAttributes>? = nil
}
extension MainView: View {
var body: some View {
VStack(spacing: 16) {
Button("Start Activity") {
startActivity()
}
Button("Stop Activity") {
stopActivity()
}
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
func startActivity() {
let attributes = TimerAttributes(timerName: "Dummy Timer")
let state = TimerAttributes.TimerStatus(endTime: Date().addingTimeInterval(60 * 5))
do{
activity = try Activity.request(attributes: attributes, contentState: state)
}
catch (let error) {
print("")
print("$$$$$$$$$$")
print(error.localizedDescription)
print(error)
print(error.self)
print("$$$$$$$$$$")
print("")
}
}
func stopActivity() {
let state = TimerAttributes.TimerStatus(endTime: .now)
Task {
await activity?.end(using: state, dismissalPolicy: .immediate)
}
}
func updateActivity() {
let state = TimerAttributes.TimerStatus(endTime: Date().addingTimeInterval(60 * 10))
Task {
await activity?.update(using: state)
}
}
}
It's looks fine, but it doesn't work at all.
I'm using a widget as new target to my main App.
I'm set NSSupportsLiveActivities to YES in both Info.plist.
What I get all time after pressing the button to start activity:
Any suggestions?
I Found a Solution.
You need to Update your Xcode to version 14.1, and recreate your widget, you will find out that checkbox 'Live Activities' on create phase appears, and give you a possibility to create Live Activities.

Using NavigationLink programmatically fails on iPadOS when using #StateObject in detail view

The following code works perfectly fine on iOS, but not on iPadOS. When I tap on one of the items in the list, the corresponding detail view is shown, but it will not change if I tap on another item. When I change the model in the LanguageDetail view to #ObservedObject, it works. To be clear, this is only an example to illustrate the problem. In my actual project, I'm not able to make this change though. The code below demonstrates this problem.
struct ContentView: View {
let languages: [String] = ["Objective-C", "Java", "Python", "Swift", "Rust"]
#State var selectedLanguage: String?
var body: some View {
NavigationView {
List(languages, id: \.self) { language in
Button(action: {selectedLanguage = language}) {
Text(language)
.bold()
.padding()
}
}
.background {
NavigationLink(isActive: $selectedLanguage.isPresent()) {
if let lang = selectedLanguage {
LanguageDetail(model: LanguageDetailModel(languageName: lang))
} else {
EmptyView()
}
} label: {
EmptyView()
}
}
}
}
}
struct LanguageDetail: View {
#StateObject var model: LanguageDetailModel
var body: some View {
VStack {
Text(model.languageName)
.font(.headline)
Text("to rule them all...")
}
}
}
class LanguageDetailModel: ObservableObject {
#Published var languageName: String
init(languageName: String) {
self.languageName = languageName
}
}
This extension is needed:
/// This extension is from the [SwiftUI Navigation Project on Github](https://github.com/pointfreeco/swiftui-navigation)
extension Binding {
/// Creates a binding by projecting the current optional value to a boolean describing if it's
/// non-`nil`.
///
/// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing.
///
/// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`.
public func isPresent<Wrapped>() -> Binding<Bool>
where Value == Wrapped? {
.init(
get: { self.wrappedValue != nil },
set: { isPresent, transaction in
if !isPresent {
self.transaction(transaction).wrappedValue = nil
}
}
)
}
}
import SwiftUI
enum Language: Int, Identifiable, CaseIterable {
case objc
case java
case python
case swift
case rust
var id: Self { self }
var localizedDescription: LocalizedStringKey {
switch self {
case .objc: return "Objective-C"
case .java: return "Java"
case .python: return "Python"
case .swift: return "Swift"
case .rust: return "Rust"
}
}
}
struct LanguagesTest: View {
#State private var selection: Language?
var body: some View {
NavigationSplitView {
List(Language.allCases, selection: $selection) { language in
NavigationLink(value: language) {
Text(language.localizedDescription)
}
}
} detail: {
if let language = selection {
LanguageDetail(language: language)
}
else {
Text("Select Language")
}
}
}
}
struct LanguageDetail: View {
let language: Language
var body: some View {
VStack {
Text(language.localizedDescription)
.font(.headline)
Text("to rule them all...")
}
}
}

Encoding to JSON format is not encoding the toggled boolean value in Swift

I am making an app that has information about different woods, herbs and spices, and a few other things. I am including the ability to save their favorite item to a favorites list, so I have a heart button that the user can press to add it to the favorites. Pressing the button toggles the isFavorite property of the item and then leaving the page calls a method that encodes the data to save it to the user's device. The problem that I am running into is that it is not encoding the updated value of the isFavorite property. It is still encoding the value as false, so the favorites list is not persisting after closing and reopening the app.
Here is my Wood.swift code, this file sets up the structure for Wood items. I also included the test data that I was using to make sure that it displayed properly in the Wood extension:
import Foundation
struct Wood: Identifiable, Codable {
var id = UUID()
var mainInformation: WoodMainInformation
var preparation: [Preparation]
var isFavorite = false
init(mainInformation: WoodMainInformation, preparation: [Preparation]) {
self.mainInformation = mainInformation
self.preparation = preparation
}
}
struct WoodMainInformation: Codable {
var category: WoodCategory
var description: String
var medicinalUses: [String]
var magicalUses: [String]
var growZone: [String]
var lightLevel: String
var moistureLevel: String
var isPerennial: Bool
var isEdible: Bool
}
enum WoodCategory: String, CaseIterable, Codable {
case oak = "Oak"
case pine = "Pine"
case cedar = "Cedar"
case ash = "Ash"
case rowan = "Rowan"
case willow = "Willow"
case birch = "Birch"
}
enum Preparation: String, Codable {
case talisman = "Talisman"
case satchet = "Satchet"
case tincture = "Tincture"
case salve = "Salve"
case tea = "Tea"
case ointment = "Ointment"
case incense = "Incense"
}
extension Wood {
static let woodTypes: [Wood] = [
Wood(mainInformation: WoodMainInformation(category: .oak,
description: "A type of wood",
medicinalUses: ["Healthy", "Killer"],
magicalUses: ["Spells", "Other Witchy Stuff"],
growZone: ["6A", "5B"],
lightLevel: "Full Sun",
moistureLevel: "Once a day",
isPerennial: false,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet]),
Wood(mainInformation: WoodMainInformation(category: .pine,
description: "Another type of wood",
medicinalUses: ["Healthy"],
magicalUses: ["Spells"],
growZone: ["11G", "14F"],
lightLevel: "Full Moon",
moistureLevel: "Twice an hour",
isPerennial: true,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet])
]
}
Here is my WoodData.swift file, this file contains methods that allow the app to display the correct wood in the list of woods, as well as encode, and decode the woods:
import Foundation
class WoodData: ObservableObject {
#Published var woods = Wood.woodTypes
var favoriteWoods: [Wood] {
woods.filter { $0.isFavorite }
}
func woods(for category: WoodCategory) -> [Wood] {
var filteredWoods = [Wood]()
for wood in woods {
if wood.mainInformation.category == category {
filteredWoods.append(wood)
}
}
return filteredWoods
}
func woods(for category: [WoodCategory]) -> [Wood] {
var filteredWoods = [Wood]()
filteredWoods = woods
return filteredWoods
}
func index(of wood: Wood) -> Int? {
for i in woods.indices {
if woods[i].id == wood.id {
return i
}
}
return nil
}
private var dataFileURL: URL {
do {
let documentsDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
return documentsDirectory.appendingPathComponent("evergreenData")
}
catch {
fatalError("An error occurred while getting the url: \(error)")
}
}
func saveWoods() {
if let encodedData = try? JSONEncoder().encode(woods) {
do {
try encodedData.write(to: dataFileURL)
let string = String(data: encodedData, encoding: .utf8)
print(string)
}
catch {
fatalError("An error occurred while saving woods: \(error)")
}
}
}
func loadWoods() {
guard let data = try? Data(contentsOf: dataFileURL) else { return }
do {
let savedWoods = try JSONDecoder().decode([Wood].self, from: data)
woods = savedWoods
}
catch {
fatalError("An error occurred while loading woods: \(error)")
}
}
}
Finally, this is my WoodsDetailView.swift file, this file displays the information for the wood that was selected, as well as calls the method that encodes the wood data:
import SwiftUI
struct WoodsDetailView: View {
#Binding var wood: Wood
#State private var woodsData = WoodData()
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses.indices, id: \.self) { index in
let medicinalUse = wood.mainInformation.medicinalUses[index]
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses.indices, id: \.self) { index in
let magicalUse = wood.mainInformation.magicalUses[index]
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone.indices, id: \.self) { index in
let zone = wood.mainInformation.growZone[index]
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
wood.isFavorite.toggle()
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
#State static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: $wood)
}
}
This is my MainTabView.swift file:
import SwiftUI
struct MainTabView: View {
#StateObject var woodData = WoodData()
var body: some View {
TabView {
NavigationView {
List {
WoodsListView(viewStyle: .allCategories(WoodCategory.allCases))
}
}
.tabItem { Label("Main", systemImage: "list.dash")}
NavigationView {
List {
WoodsListView(viewStyle: .favorites)
}
.navigationTitle("Favorites")
}.tabItem { Label("Favorites", systemImage: "heart.fill")}
}
.environmentObject(woodData)
.onAppear {
woodData.loadWoods()
}
.preferredColorScheme(.dark)
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView()
}
}
This is my WoodListView.swift file:
import SwiftUI
struct WoodsListView: View {
#EnvironmentObject private var woodData: WoodData
let viewStyle: ViewStyle
var body: some View {
ForEach(woods) { wood in
NavigationLink(wood.mainInformation.category.rawValue, destination: WoodsDetailView(wood: binding(for: wood)))
}
}
}
extension WoodsListView {
enum ViewStyle {
case favorites
case singleCategory(WoodCategory)
case allCategories([WoodCategory])
}
private var woods: [Wood] {
switch viewStyle {
case let .singleCategory(category):
return woodData.woods(for: category)
case let .allCategories(category):
return woodData.woods(for: category)
case .favorites:
return woodData.favoriteWoods
}
}
func binding(for wood: Wood) -> Binding<Wood> {
guard let index = woodData.index(of: wood) else {
fatalError("Wood not found")
}
return $woodData.woods[index]
}
}
struct WoodsListView_Previews: PreviewProvider {
static var previews: some View {
WoodsListView(viewStyle: .singleCategory(.ash))
.environmentObject(WoodData())
}
}
Any assistance into why it is not encoding the toggled isFavorite property will be greatly appreciated.
Your problem is that structs are value types in Swift. Essentially this means that the instance of Wood that you have in WoodsDetailView is not the same instance that is in your array in your model (WoodData); It is a copy (Technically, the copy is made as soon as you modify the isFavourite property).
In SwiftUI it is important to maintain separation of responsibilities between the view and the model.
Changing the favourite status of a Wood is something the view should ask the model to do.
This is where you have a second issue; In your detail view you are creating a separate instance of your model; You need to refer to a single instance.
You have a good start; you have put your model instance in the environment where views can access it.
First, change the detail view to remove the binding, refer to the model from the environment and ask the model to do the work:
struct WoodsDetailView: View {
var wood: Wood
#EnvironmentObject private var woodsData: WoodData
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses, id: \.self) { medicinalUse in
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses, id: \.self) { magicalUse in
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone, id: \.self) { zone in
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
self.woodsData.toggleFavorite(for: wood)
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: wood)
}
}
I also got rid of the unnecessary use of indices when listing the properties.
Now, add a toggleFavorite function to your WoodData object:
func toggleFavorite(for wood: Wood) {
guard let index = self.woods.firstIndex(where:{ $0.id == wood.id }) else {
return
}
self.woods[index].isFavorite.toggle()
}
You can also remove the index(of wood:Wood) function (which was really just duplicating Array's firstIndex(where:) function) and the binding(for wood:Wood) function.
Now, not only does your code do what you want, but you have hidden the mechanics of toggling a favorite from the view; It simply asks for the favorite status to be toggled and doesn't need to know what this actually involves.

UICloudSharingController Does not Display/Work with CloudKit App

I have been struggling to get an app to work with CloudKit and record sharing. I have
created several apps that sync Core Data records among devices for one user. I have not
been able to get the UICloudSharingController to work for sharing records. I can display
the Sharing View Controller, but tapping on Mail or Message displays a keyboard but no
address field and no way to dismiss the view. I have been so frustrated by it that I
decided to try the Apple "Sharing" sample app to start from the basics. However, the
sample app does not work for me either.
Here's the link to the sample app:
https://github.com/apple/cloudkit-sample-sharing/tree/swift-concurrency
The code below is pretty much straight from the sample app.
This is the ContentView file:
import SwiftUI
import CloudKit
struct ContentView: View {
#EnvironmentObject private var vm: ViewModel
#State private var isAddingContact = false
#State private var isSharing = false
#State private var isProcessingShare = false
#State private var showShareView = false
#State private var showIntermediateView = false
#State private var activeShare: CKShare?
#State private var activeContainer: CKContainer?
var body: some View {
NavigationView {
contentView
.navigationTitle("Contacts")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button { Task.init { try await vm.refresh() } } label: { Image(systemName: "arrow.clockwise") }
}
ToolbarItem(placement: .navigationBarLeading) {
progressView
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { isAddingContact = true }) { Image(systemName: "plus") }
}
}
}
.onAppear {
Task.init {
try await vm.initialize()
try await vm.refresh()
}
}
.sheet(isPresented: $isAddingContact, content: {
AddContactView(onAdd: addContact, onCancel: { isAddingContact = false })
})
}
/// This progress view will display when either the ViewModel is loading, or a share is processing.
var progressView: some View {
let showProgress: Bool = {
if case .loading = vm.state {
return true
} else if isProcessingShare {
return true
}
return false
}()
return Group {
if showProgress {
ProgressView()
}
}
}
/// Dynamic view built from ViewModel state.
private var contentView: some View {
Group {
switch vm.state {
case let .loaded(privateContacts, sharedContacts):
List {
Section(header: Text("Private")) {
ForEach(privateContacts) { contactRowView(for: $0) }
}
Section(header: Text("Shared")) {
ForEach(sharedContacts) { contactRowView(for: $0, shareable: false) }
}
}.listStyle(GroupedListStyle())
case .error(let error):
VStack {
Text("An error occurred: \(error.localizedDescription)").padding()
Spacer()
}
case .loading:
VStack { EmptyView() }
}
}
}
/// Builds a `CloudSharingView` with state after processing a share.
private func shareView() -> CloudSharingView? {
guard let share = activeShare, let container = activeContainer else {
return nil
}
return CloudSharingView(container: container, share: share)
}
/// Builds a Contact row view for display contact information in a List.
private func contactRowView(for contact: Contact, shareable: Bool = true) -> some View {
HStack {
VStack(alignment: .leading) {
Text(contact.name)
Text(contact.phoneNumber)
.textContentType(.telephoneNumber)
.font(.footnote)
}
if shareable {
Spacer()
Button(action: { Task.init { try? await shareContact(contact) }
}, label: { Image(systemName: "square.and.arrow.up") }).buttonStyle(BorderlessButtonStyle())
.sheet(isPresented: $isSharing, content: { shareView() })
}//if sharable
}//h
}//contact row view
// MARK: - Actions
private func addContact(name: String, phoneNumber: String) async throws {
try await vm.addContact(name: name, phoneNumber: phoneNumber)
try await vm.refresh()
isAddingContact = false
}
private func shareContact(_ contact: Contact) async throws {
isProcessingShare = true
do {
let (share, container) = try await vm.createShare(contact: contact)
isProcessingShare = false
activeShare = share
activeContainer = container
isSharing = true
} catch {
debugPrint("Error sharing contact record: \(error)")
}
}
}
And the UIViewControllerRepresentable file for the sharing view:
import Foundation
import SwiftUI
import UIKit
import CloudKit
/// This struct wraps a `UIImagePickerController` for use in SwiftUI.
struct CloudSharingView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
let container: CKContainer
let share: CKShare
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
func makeUIViewController(context: Context) -> some UIViewController {
let sharingController = UICloudSharingController(share: share, container: container)
sharingController.availablePermissions = [.allowReadWrite, .allowPrivate]
sharingController.delegate = context.coordinator
return sharingController
}
func makeCoordinator() -> CloudSharingView.Coordinator {
Coordinator()
}
final class Coordinator: NSObject, UICloudSharingControllerDelegate {
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
debugPrint("Error saving share: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
"Sharing Example"
}
}
}
This is the presented screen when tapping to share a record. This all looks as expected:
This is the screen after tapping Messages (same result for Mail). I don't see any way to influence the second screen presentation - it seems that the view controller representable is not working with this version of Xcode:
Any guidance would be appreciated. Xcode 13.1, iOS 15 and I am on macOS Monterrey.
I had the same issue and fixed it by changing makeUIViewController() in CloudSharingView.swift:
func makeUIViewController(context: Context) -> some UIViewController {
share[CKShare.SystemFieldKey.title] = "Boom"
let sharingController = UICloudSharingController(share: share, container: container)
sharingController.availablePermissions = [.allowReadWrite, .allowPrivate]
sharingController.delegate = context.coordinator
-->>> sharingController.modalPresentationStyle = .none
return sharingController
}
It seems like some value work, some don't. Not sure why.

How to use #FocusedValue with optionals?

This question is similar to this unanswered question from the Apple Developer Forums, but with a slightly different scenario:
I have a view with a #FetchRequest property of type FetchedResults<Metric>
Within the view, I display the list of those objects
I can tap on one item to select it, storing that selection in a #State var selection: Metric? = nil property.
Here's the properties I defined for my #FocusedValue:
struct FocusedMetricValue: FocusedValueKey {
typealias Value = Metric?
}
extension FocusedValues {
var metricValue: FocusedMetricValue.Value? {
get { self[FocusedMetricValue.self] }
set { self[FocusedMetricValue.self] = newValue }
}
}
Here's how I set the focusedValue from my list view:
.focusedValue(\.metricValue, selection)
And here's how I'm using the #FocusedValue on my Commands struct:
struct MacOSCommands: Commands {
#FocusedValue(\.metricValue) var metric
var body: some Commands {
CommandMenu("Metric") {
Button("Test") {
print(metric??.name ?? "-")
}
.disabled(metric == nil)
}
}
}
The code builds successfully, but when I run the app and select a Metric from the list, the app freezes. If I pause the program execution in Xcode, this is the stack trace I get:
So, how can I make #FocusedValue work in this scenario, with an optional object from a list?
I ran into the same issue. Below is a View extension and ViewModifier that present a version of focusedValue which accepts an Binding to an optional. Not sure why this wasn't included in the framework as it corresponds more naturally to a selection situation in which there can be none...
extension View{
func focusedValue<T>(_ keypath: WritableKeyPath<FocusedValues, Binding<T>?>, selection: Binding<T?>) -> some View{
self.modifier(FocusModifier(keypath: keypath, optionalBinding: selection))
}
}
struct FocusModifier<T>: ViewModifier{
var keypath: WritableKeyPath<FocusedValues, Binding<T>?>
var optionalBinding: Binding<T?>
func body(content: Content) -> some View{
Group{
if optionalBinding.wrappedValue == nil{
content
}
else if let binding = Binding(optionalBinding){
content
.focusedValue(keypath, binding)
}
else{
content
}
}
}
}
In your car usage would look like:
.focusedValue(\.metricValue, selection: $selection)
I have also found that the placement of this statement is finicky. I can only make things work when I place this on the NavigationView itself as opposed to one of its descendants (eg List).
// 1 CoreData optional managedObject in #State var selection
#State var selection: Metric?
// 2 modifiers on View who own the list with the selection
.focusedValue(\.metricValue, $selection)
.focusedValue(\.deleteMetricAction) {
if let metric = selection {
print("Delete \(metric.name ?? "unknown metric")?")
}
}
// 3
extension FocusedValues {
private struct SelectedMetricKey: FocusedValueKey {
typealias Value = Binding<Metric?>
}
private struct DeleteMetricActionKey: FocusedValueKey {
typealias Value = () -> Void
}
var metricValue: Binding<Metric?>? {
get { self[SelectedMetricKey.self] }
set { self[SelectedMetricKey.self] = newValue}
}
var deleteMetricAction: (() -> Void)? {
get { self[DeleteMetricActionKey.self] }
set { self[DeleteMetricActionKey.self] = newValue }
}
}
// 4 Use in macOS Monterey MenuCommands
struct MetricsCommands: Commands {
#FocusedValue(\.metricValue) var selectedMetric
#FocusedValue(\.deleteMetricAction) var deleteMetricAction
var body: some Commands {
CommandMenu("Type") {
Button { deleteMetricAction?() } label: { Text("Delete \(selectedMetric.name ?? "unknown")") }.disabled(selectedMetric?.wrappedValue == nil || deleteMetricAction == nil )
}
}
}
// 5 In macOS #main App struct
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
.commands {
SidebarCommands()
MetricsCommands()
}
}
Use of #FocusedValues in Apple WWDC21 example
Use of FocusedValues in SwiftUI with Majid page
Use of #FocusedSceneValue with example of action value passed, in Apple documentation
For SwiftUI 3 macOS Table who support multiple selections
// 1 Properties
#Environment(\.managedObjectContext) var context
var type: Type
var fetchRequest: FetchRequest<Propriete>
var proprietes: FetchedResults<Propriete> { fetchRequest.wrappedValue }
#State private var selectedProprietes = Set<Propriete.ID>()
// 2 Init from Type managedObject who own Propriete managedObjects
// #FecthRequest required to have updates in Table (when delete for example)
init(type: Type) {
self.type = type
fetchRequest = FetchRequest<Propriete>(entity: Propriete.entity(),
sortDescriptors: [ NSSortDescriptor(key: "nom",
ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:))) ],
predicate: NSPredicate(format: "type == %#", type))
}
// 3 Table view
VStack {
Table(proprietes, selection: $selectedProprietes) {
TableColumn("Propriétés :", value: \.wrappedNom)
}
.tableStyle(BorderedTableStyle())
.focusedSceneValue(\.selectedProprietes, $selectedProprietes)
.focusedSceneValue(\.deleteProprietesAction) {
deleteProprietes(selectedProprietes)
}
}
// 4 FocusedValues
extension FocusedValues {
private struct FocusedProprietesSelectionKey: FocusedValueKey {
typealias Value = Binding<Set<Propriete.ID>>
}
var selectedProprietes: Binding<Set<Propriete.ID>>? {
get { self[FocusedProprietesSelectionKey.self] }
set { self[FocusedProprietesSelectionKey.self] = newValue }
}
}
// 5 Delete (for example) in Table View
private func deleteProprietes(_ proprietesToDelete: Set<Propriete.ID>) {
var arrayToDelete = [Propriete]()
for (index, propriete) in proprietes.enumerated() {
if proprietesToDelete.contains(propriete.id) {
let propriete = proprietes[index]
arrayToDelete.append(propriete)
}
}
if arrayToDelete.count > 0 {
print("array to delete: \(arrayToDelete)")
for item in arrayToDelete {
context.delete(item)
print("\(item.wrappedNom) deleted!")
}
try? context.save()
}
}
How to manage selection in Table

Resources