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.
Related
I have a reminder app that I am trying to implement persistent data in but whenever I close the app no data is saved. I know how to make it work with a normal MVC but I would like to get it working with the view model that I have.
I think I know what needs to change to fix the problem but I am not sure how to get to the solution. I am pretty sure that in the ReminderApp file under the NavigationView where it says HomeViewModel(reminds: store.reminds) I think that the store.reminds part needs to be binded to with a $ at the beginning but when I try doing that it doesn't work and instead says that HomeViewModel reminds property expects Reminder instead of Binding.
ReminderStore loads and saves the reminders to a file with the reminders and HomeViewModel contains the reminders array and appends a reminder to the array when a user adds a new reminder.
If anyone knows how to get this working that would be great since I have been stuck on this. My minimal reproducable example code is below.
RemindersApp
import SwiftUI
#main
struct RemindersApp: App {
#StateObject private var store = ReminderStore()
var body: some Scene {
WindowGroup {
NavigationView {
HomeView(homeVM: HomeViewModel(reminds: store.reminds)) {
ReminderStore.save(reminds: store.reminds) { result in
if case .failure(let error) = result {
fatalError(error.localizedDescription)
}
}
}
.navigationBarHidden(true)
}
.onAppear {
ReminderStore.load { result in
switch result {
case .failure(let error):
fatalError(error.localizedDescription)
case .success(let reminds):
store.reminds = reminds
}
}
}
}
}
}
HomeView
import SwiftUI
struct HomeView: View {
#StateObject var homeVM: HomeViewModel
#Environment(\.scenePhase) private var scenePhase
#State var addView = false
let saveAction: ()->Void
var body: some View {
VStack {
List {
ForEach($homeVM.reminds) { $remind in
Text(remind.title)
}
}
}
.safeAreaInset(edge: .top) {
HStack {
Text("Reminders")
.font(.title)
.padding()
Spacer()
Button(action: {
addView.toggle()
}) {
Image(systemName: "plus")
.padding()
.font(.title2)
}
.sheet(isPresented: $addView) {
NavigationView {
VStack {
Form {
TextField("Title", text: $homeVM.newRemindData.title)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
homeVM.newRemindData = Reminder.Data()
addView.toggle()
}
}
ToolbarItem(placement: .principal) {
Text("New Reminder")
.font(.title3)
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
homeVM.addRemindData(remindData: homeVM.newRemindData)
addView.toggle()
}
}
}
}
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView(homeVM: HomeViewModel(reminds: Reminder.sampleReminders), saveAction: {})
}
}
ReminderStore
import Foundation
import SwiftUI
class ReminderStore: ObservableObject {
#Published var reminds: [Reminder] = []
private static func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("reminds.data")
}
static func load(completion: #escaping (Result<[Reminder], Error>) -> Void) {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
DispatchQueue.main.async {
completion(.success([]))
}
return
}
let reminds = try JSONDecoder().decode([Reminder].self, from: file.availableData)
DispatchQueue.main.async {
completion(.success(reminds))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
static func save(reminds: [Reminder], completion: #escaping (Result<Int, Error>) -> Void) {
do {
let data = try JSONEncoder().encode(reminds)
let outfile = try fileURL()
try data.write(to: outfile)
DispatchQueue.main.async {
completion(.success(reminds.count))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
HomeViewModel
import Foundation
class HomeViewModel: ObservableObject {
#Published var reminds: [Reminder]
#Published var newRemindData = Reminder.Data()
init(reminds: [Reminder]) {
self.reminds = reminds
}
func addRemindData(remindData: Reminder.Data) {
let newRemind = Reminder(data: remindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
}
Reminder
import Foundation
struct Reminder: Identifiable, Codable {
var title: String
let id: UUID
init(title: String, id: UUID = UUID()) {
self.title = title
self.id = id
}
}
extension Reminder {
struct Data {
var title: String = ""
var id: UUID = UUID()
}
var data: Data {
Data(title: title)
}
mutating func update(from data: Data) {
title = data.title
}
init(data: Data) {
title = data.title
id = UUID()
}
}
extension Reminder {
static var sampleReminders = [
Reminder(title: "Reminder1"),
Reminder(title: "Reminder2"),
Reminder(title: "Reminder3")
]
}
The reason you are struggeling here is because you try to have multiple Source of truth.
documentation on dataflow in SwiftUI
You should move the code from HomeViewModel to your ReminderStore and change the static functions to instance functions. This would keep your logic in one place.
You can pass your ReminderStore to your HomeView as an #EnvironmentObject
This would simplify your code to:
class ReminderStore: ObservableObject {
#Published var reminds: [Reminder] = []
#Published var newRemindData = Reminder.Data()
private func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("reminds.data")
}
func load() {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try self.fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
return
}
let reminds = try JSONDecoder().decode([Reminder].self, from: file.availableData)
DispatchQueue.main.async {
self.reminds = reminds
}
} catch {
DispatchQueue.main.async {
fatalError(error.localizedDescription)
}
}
}
}
func save() {
do {
let data = try JSONEncoder().encode(reminds)
let outfile = try fileURL()
try data.write(to: outfile)
} catch {
fatalError(error.localizedDescription)
}
}
func addRemindData() {
let newRemind = Reminder(data: newRemindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
}
struct RemindersApp: App {
#StateObject private var store = ReminderStore()
var body: some Scene {
WindowGroup {
NavigationView {
HomeView() {
store.save()
}
.navigationBarHidden(true)
.environmentObject(store)
}
.onAppear {
store.load()
}
}
}
}
struct HomeView: View {
#Environment(\.scenePhase) private var scenePhase
#EnvironmentObject private var store: ReminderStore
#State var addView = false
let saveAction: ()->Void
var body: some View {
VStack {
List {
ForEach(store.reminds) { remind in
Text(remind.title)
}
}
}
.safeAreaInset(edge: .top) {
HStack {
Text("Reminders")
.font(.title)
.padding()
Spacer()
Button(action: {
addView.toggle()
}) {
Image(systemName: "plus")
.padding()
.font(.title2)
}
.sheet(isPresented: $addView) {
NavigationView {
VStack {
Form {
TextField("Title", text: $store.newRemindData.title)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
store.newRemindData = Reminder.Data()
addView.toggle()
}
}
ToolbarItem(placement: .principal) {
Text("New Reminder")
.font(.title3)
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
store.addRemindData()
addView.toggle()
}
}
}
}
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
}
}
}
An issue I would recommend solving:
Naming a type after something that´s allready taken by Swift is a bad idea. You should rename your Data struct to something different.
ReminderStore.save isn't invoking in time.
By the time it invokes it doesn't have/get the reminder data.
That's the first thing I would make sure gets done. You may end up running into other issues afterward, but I would personally focus on that first.
I'm trying to learn SwiftUI, but i can't seem to get my view to update. I want my WorkoutsView to refresh with the newly added workout when the user presses the "Add" button:
WorkoutTrackerApp:
#main
struct WorkoutTrackerApp: App {
var body: some Scene {
WindowGroup {
WorkoutTrackerView()
}
}
}
extension WorkoutTrackerApp {
struct WorkoutTrackerView: View {
#StateObject var workoutService = WorkoutService.instance
var body: some View {
NavigationView {
WorkoutsView { $workout in
NavigationLink(destination: WorkoutView(workout: $workout)){
Text(workout.title)
}
}
.toolbar {
Button("Add") {
workoutService.addNewWorkout()
}
}
.navigationTitle("Workouts")
}
.environmentObject(workoutService)
}
}
}
WorkoutsView:
import Foundation
import SwiftUI
struct WorkoutsView<Wrapper>: View where Wrapper: View {
#EnvironmentObject var workoutService: WorkoutService
#StateObject var viewModel: ViewModel
let workoutWrapper: (Binding<Workout>) -> Wrapper
init(_ viewModel: ViewModel = .init(), workoutWrapper: #escaping (Binding<Workout>) -> Wrapper) {
_viewModel = StateObject(wrappedValue: viewModel)
self.workoutWrapper = workoutWrapper
}
var body: some View {
List {
Section(header: Text("All Workouts")) {
ForEach($viewModel.workouts) { $workout in
workoutWrapper($workout)
}
}
}
.onAppear {
viewModel.workoutService = self.workoutService
viewModel.getWorkouts()
}
}
}
extension WorkoutsView {
class ViewModel: ObservableObject {
#Published var workouts = [Workout]()
var workoutService: WorkoutService?
func getWorkouts() {
workoutService?.getWorkouts { workouts in
self.workouts = workouts
}
}
}
}
WorkoutService:
import Foundation
class WorkoutService: ObservableObject {
static let instance = WorkoutService()
#Published var workouts = [Workout]()
private init() {
for i in 0...5 {
let workout = Workout(id: i, title: "Workout \(i)", exercises: [])
workouts.append(workout)
}
}
func getWorkouts(completion: #escaping ([Workout]) -> Void) {
DispatchQueue.main.async {
completion(self.workouts)
}
}
func addNewWorkout() {
let newWorkout = Workout(title: "New Workout")
workouts = workouts + [newWorkout]
}
}
The .onAppear in WorkoutsView only gets called once - when the view gets initialised for the first time. I want it to also get triggered when workoutService.addNewWorkout() gets called.
FYI: The WorkoutService is a 'mock' service, in the future i want to call an API there.
Figured it out, changed the body of WorkoutsView to this:
var body: some View {
List {
Section(header: Text("All Workouts")) {
ForEach($viewModel.workouts) { $workout in
workoutWrapper($workout)
}
}
}
.onAppear {
viewModel.workoutService = self.workoutService
viewModel.getWorkouts()
}
.onReceive(workoutService.objectWillChange) {
viewModel.getWorkouts()
}
}
Now the workouts list gets refreshed when workoutService publisher emits. The solution involved using the .onReceive to do something when the WorkoutService changes.
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.
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.
So, I have the following View Model where I fetch the data:
import Foundation
import SwiftUI
import Combine
import Alamofire
class AllStatsViewModel: ObservableObject {
#Published var isLoading: Bool = true
#Published var stats = [CountryStats]()
func fetchGlobalStats() {
let request = AF.request("https://projectcovid.deadpool.wtf/all")
request.responseDecodable(of: AllCountryStats.self) { (response) in
guard let globalStats = response.value else { return }
DispatchQueue.main.async {
self.stats = globalStats.data
}
self.isLoading = false
}
}
}
And this is the view:
struct CardView: View {
#ObservedObject var allStatsVM = AllStatsViewModel()
var body: some View {
VStack {
Text(self.allStatsVM.stats[0].country)
}
.onAppear {
self.allStatsVM.fetchGlobalStats()
}
}
}
I'd like to access only the first element of the data, but the problem I face is that when the view loads, the data is not loaded, so I get an index out of range error at
Text(self.allStatsVM.stats[0].country)
Is there a way, I can access the first element?
try this:
struct CardView: View {
#ObservedObject var allStatsVM = AllStatsViewModel()
var body: some View {
VStack {
if self.allStatsVM.stats.count > 0 {
Text(self.allStatsVM.stats[0].country)
} else {
Text ("data loading")
}
}
.onAppear {
self.allStatsVM.fetchGlobalStats()
}
}
}