Hi there,
I'm using WCSession to communicate watchOS companion with its iOS app.
Every time watch app becomes "active", it needs to fetch data from iOS app, which works e.g. turning my hand back and forth.
But only when the app is opened after it was minimised by pressing digital crown, it didn't fetch data. My assumption is that scenePhase doesn't emit a change on reopen.
Here is the ContentView of watch app:
import SwiftUI
struct ContentView: View {
@EnvironmentObject private var iOSAppConnector: IOSAppConnector
@Environment(\.scenePhase) private var scenePhase
@State private var showOpenCategories = true
var body: some View {
NavigationStack {
VStack {
if iOSAppConnector.items.isEmpty {
WelcomeView()
} else {
ScrollView {
VStack(spacing: 10) {
ForEach(iOSAppConnector.items, id: \.self.name) { item in
ItemView(item: item)
}
}
}
.task {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
loadItems()
}
}
.onChange(of: scenePhase, initial: true) { newPhase, _ in
if newPhase == .active {
loadItems()
}
}
}
fileprivate func loadItems() -> Void {
if iOSAppConnector.items.isEmpty {
iOSAppConnector.loadItems()
}
}
}
What could be the issue?
Thanks.
Best regards
Sanjeev
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hi everyone, I'm using an app group to share data between iOS and it's watch companion app.
I ensured that is has the same identifier in Signing & Capabilities and in the .entitlements files.
Here is the UserDefaults part:
class UserDefaultsManager {
private let suitName = "group.com.sanjeevbalakrishnan.Test"
public func saveItems(_ items: [ItemDTO]) {
print("Save \(items.count) items to shared defaults")
let defaults = UserDefaults(suiteName: suitName)
let data = try? JSONEncoder().encode(items)
defaults?.set(data, forKey: "items")
}
public func loadItems() -> [ItemDTO] {
let defaults = UserDefaults(suiteName: suitName)
print(defaults)
guard let data = defaults?.data(forKey: "items") else {
print("watchOS received data is empty")
return []
}
let items = [ItemDTO].from(data: data)
print("Load \(items.count) items from user defaults")
return items
}
}
For testing I called loadItems after saveItems on iOS app and it returned items. However, on watchOS app it always returns empty array.
What do I need to consider?
Thanks.
Best regards
Sanjeev
I have a UIApplicationDelegate, where I want to terminate a live activity (in dynamic island) in applicationWillTerminate. However, ending an activity is asynchronous. When I end it within a Task, it's never called.
Problem: When clicking on an item, it will be (un-)completed. When it's completed then ChildView should show GroupsView. However, that's not the case.
The logs are like this:
init, group Fruits - items not completed false - showItems false
body, group Fruits - items not completed false - showItems true
init, group Fruits - items not completed false - showItems false
init, group Fruits - items not completed false - showItems false
import SwiftUI
import SwiftData
@main
struct MainApp: App {
var body: some Scene {
WindowGroup {
SomeView()
}
.modelContainer(appContainer)
}
}
struct SomeView: View {
@Query private var items: [AItem]
var body: some View {
ParentView(items: items)
}
}
struct ParentView: View {
private var groupedItems: [GroupedAItems] = []
init(items: [AItem]) {
Dictionary(grouping: items) { $0.categoryName }
.forEach {
let groupedItems = GroupedAItems(categoryName: $0.key, items: $0.value)
self.groupedItems.append(groupedItems)
}
}
var body: some View {
ScrollView {
VStack(spacing: 15) {
ForEach(groupedItems, id: \.self.categoryName) { groupedItems in
ChildView(groupedItems)
}
}
}
}
}
struct ChildView: View {
public var groupedItems: GroupedAItems
@State private var showItems: Bool
init(_ groupedItems: GroupedAItems) {
self.groupedItems = groupedItems
self._showItems = State(initialValue: !groupedItems.completed)
print("init, group \(groupedItems.categoryName) - items not completed \(!groupedItems.completed) - showItems \(showItems)")
}
var body: some View {
print("body, group \(groupedItems.categoryName) - items not completed \(!groupedItems.completed) - showItems \(showItems)")
if showItems {
return AnyView(ItemsSampleView(items: groupedItems.items, onClick: { showItems = false }))
} else {
return AnyView(GroupsView(groupedItems: groupedItems, onClick: { showItems = true }))
}
}
}
struct ItemsSampleView: View {
public var items: [AItem]
public var onClick: () -> Void
private let gridColumns = [GridItem(.adaptive(minimum: CGFloat(70)))]
var body: some View {
VStack {
Button {
onClick()
} label: {
Image(systemName: "chevron.down")
}
Spacer()
LazyVGrid(columns: gridColumns) {
ForEach(items.sorted(by: {$0.name < $1.name})) { item in
Button {
item.completed.toggle()
} label: {
Text(item.name)
}
}
}
}
}
}
struct GroupsView: View {
public var groupedItems: GroupedAItems
public var onClick: () -> Void
var body: some View {
VStack {
Button {
onClick()
} label: {
Image(systemName: "chevron.down")
}
Spacer()
Text(groupedItems.categoryName)
}
}
}
@Model
final class AItem: Identifiable {
@Attribute(.unique) public var id: String
public var name: String
public var categoryName: String
public var completed = false
internal init(name: String, categoryName: String) {
self.id = UUID().uuidString
self.name = name
self.categoryName = categoryName
}
}
struct GroupedAItems {
var categoryName: String
var items: [AItem]
var completed: Bool {
items.filter { !$0.completed }.isEmpty
}
}
@MainActor
let appContainer: ModelContainer = {
do {
let container = try ModelContainer(for: AItem.self)
// Make sure the persistent store is empty. If it's not, return the non-empty container.
var itemFetchDescriptor = FetchDescriptor<AItem>()
itemFetchDescriptor.fetchLimit = 1
guard try container.mainContext.fetch(itemFetchDescriptor).count == 0 else { return container }
container.mainContext.insert(AItem(name: "Apple", categoryName: "Fruits"))
return container
} catch {
fatalError("Failed to create container")
}
}()
Task:
A child view should show depending on a boolean flag the corresponding view. The flag is calculated from a model which is given by parent view.
Problem:
The flag is false in init, which is correct. However in body it's true, which is incorrect. Why value in body isn't consistent to init? Is there a race condition? The child view's is rendered thrice, that's another issue, but flag is in init and body as described before.
Parent view looks like this:
struct ParentView: View {
private var groupedItems: [GroupedItems] = []
init(items: [Item]) {
Dictionary(grouping: items) { $0.category.name }
.forEach {
let groupedItems = GroupedItems(categoryName: $0.key, items: $0.value)
self.groupedItems.append(groupedItems)
}
}
var body: some View {
ChildView(groupedItems)
}
}
Child view looks like this:
struct ChildView: View {
@State private var showItems: Bool
init(_ groupedItems: GroupedItems) {
self._showItems = State(initialValue: !groupedItems.completed)
}
var body: some View {
if showItems {
AView()
} else {
BView()
}
}
}
Model looks like this:
@Model
final class Item: Identifiable {
@Attribute(.unique) public var id: String
public var name: String
public var completed = false
}
struct GroupedItems {
var categoryName: String
var items: [Item]
var completed: Bool {
items.filter { !$0.completed }.isEmpty
}
}
struct Preview: View {
private static let NAME = "Test"
@Query(filter: #Predicate<Item> { $0.name == Preview.NAME})
private var items: [Item]
var body: some View {
MyView(name: Preview.NAME, items: items)
}
}
Compile error occurs: key path cannot refer to static member 'NAME'
How can a (static) constant be referred in member closure?