carekit-apple / CareKit

CareKit is an open source software framework for creating apps that help people better understand and manage their health.
https://www.researchandcare.org
Other
2.41k stars 444 forks source link

Thread 1: signal SIGABRT #516

Closed andreaxricci closed 3 years ago

andreaxricci commented 3 years ago

Hi,

I'm trying to replicate the sample App using SwiftUI, but my code is crashing and I can't figure out why. The error I get is "Thread 1: signal SIGABRT" and is linked to the section of ContentView.swift where I define the environment values.

Could you please help me figuring out what I'm doing wrong?

Thanks in advance,

Andrea

ContentView.swift

//
//  ContentView.swift
//  Shared
//
//  Created by Andrea on 20.10.20.
//

import SwiftUI
import UIKit
import CareKit

//Adding the store manager into the App Delegate
class AppDelegate: UIResponder, UIApplicationDelegate {
    // Manages synchronization of a CoreData store
    lazy var synchronizedStoreManager: OCKSynchronizedStoreManager = {
        //let store = OCKStore(name: "SampleAppStore")
        let store = OCKStore(name: "SampleAppStore", type: .onDisk)
        store.populateSampleData()
        let manager = OCKSynchronizedStoreManager(wrapping: store)
        return manager
    }()

     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         return true
     }

     func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
         return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
     }
 }

// Extensions for Store Manager
extension EnvironmentValues {
    var storeManager: OCKSynchronizedStoreManager {
        get { self[StoreManagerKey.self] } 
        set { self[StoreManagerKey.self] = newValue }
    }
}

private struct StoreManagerKey: EnvironmentKey {
    typealias Value = OCKSynchronizedStoreManager

    // Define the value to be injected into a view's environment if no other value is explicitly set
    static var defaultValue: OCKSynchronizedStoreManager {
        let appDelegate = UIApplication.shared.delegate as! AppDelegate // <-- ERROR HERE: Thread 1: signal SIGABRT
        let manager = appDelegate.synchronizedStoreManager
        return manager
    }
}

// Hardcode some data into the Store, to test the App
private extension OCKStore {

    // Adds tasks and contacts into the store
    func populateSampleData() {

        let thisMorning = Calendar.current.startOfDay(for: Date())
        let aFewDaysAgo = Calendar.current.date(byAdding: .day, value: -4, to: thisMorning)!
        let beforeBreakfast = Calendar.current.date(byAdding: .hour, value: 8, to: aFewDaysAgo)!
        let afterLunch = Calendar.current.date(byAdding: .hour, value: 14, to: aFewDaysAgo)!

        let schedule = OCKSchedule(composing: [
            OCKScheduleElement(start: beforeBreakfast, end: nil,
                               interval: DateComponents(day: 1)),

            OCKScheduleElement(start: afterLunch, end: nil,
                               interval: DateComponents(day: 2))
        ])

        var doxylamine = OCKTask(id: "doxylamine", title: "Take Doxylamine",
                                 carePlanID: nil, schedule: schedule)
        doxylamine.instructions = "Take 25mg of doxylamine when you experience nausea."

        let nauseaSchedule = OCKSchedule(composing: [
            OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 1),
                               text: "Anytime throughout the day", targetValues: [], duration: .allDay)
            ])

        var nausea = OCKTask(id: "nausea", title: "Track your nausea",
                             carePlanID: nil, schedule: nauseaSchedule)
        nausea.impactsAdherence = false
        nausea.instructions = "Tap the button below anytime you experience nausea."

        let kegelElement = OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 2))
        let kegelSchedule = OCKSchedule(composing: [kegelElement])
        var kegels = OCKTask(id: "kegels", title: "Kegel Exercises", carePlanID: nil, schedule: kegelSchedule)
        kegels.impactsAdherence = true
        kegels.instructions = "Perform kegel exercies"

        addTasks([nausea, doxylamine, kegels], callbackQueue: .main, completion: nil)

        var contact1 = OCKContact(id: "jane", givenName: "Jane",
                                  familyName: "Daniels", carePlanID: nil)
        contact1.asset = "JaneDaniels"
        contact1.title = "Family Practice Doctor"
        contact1.role = "Dr. Daniels is a family practice doctor with 8 years of experience."

        contact1.address = {
            let address = OCKPostalAddress()
            address.street = "2598 Reposa Way"
            address.city = "San Francisco"
            address.state = "CA"
            address.postalCode = "94127"
            return address
        }()

        var contact2 = OCKContact(id: "matthew", givenName: "Matthew",
                                  familyName: "Reiff", carePlanID: nil)
        contact2.asset = "MatthewReiff"
        contact2.title = "OBGYN"
        contact2.role = "Dr. Reiff is an OBGYN with 13 years of experience."
        contact2.address = {
            let address = OCKPostalAddress()
            address.street = "396 El Verano Way"
            address.city = "San Francisco"
            address.state = "CA"
            address.postalCode = "94127"
            return address
        }()

        addContacts([contact1, contact2])
    }
}

class CareViewController: OCKDailyPageViewController {

    // Reading the store manager from the environment
    //@Environment(\.storeManager) private var storeManager

    // show the list of doctors when clicking on button "Care Team", by calling function presentContactsListViewController()
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.rightBarButtonItem =
            UIBarButtonItem(title: "Care Team", style: .plain, target: self,
                            action: #selector(presentContactsListViewController))
    }
    // definition of function presentContactsListViewController(), used to show the list of doctors. It is closed, when clicking on button "Done", which triggers function dismissContactsListViewController()
    @objc private func presentContactsListViewController() {
        let viewController = OCKContactsListViewController(storeManager: storeManager)
        viewController.title = "Care Team"
        viewController.isModalInPresentation = true
        viewController.navigationItem.rightBarButtonItem =
            UIBarButtonItem(title: "Done", style: .plain, target: self,
                            action: #selector(dismissContactsListViewController))

        let navigationController = UINavigationController(rootViewController: viewController)
        present(navigationController, animated: true, completion: nil)
    }
    // definition of function dismissContactsListViewController(), which closes the list of doctors
    @objc private func dismissContactsListViewController() {
        dismiss(animated: true, completion: nil)
    }

    // This will be called each time the selected date changes.
    // It can be used as an opportunity to rebuild the content shown to the user.
    override func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController,
                                          prepare listViewController: OCKListViewController, for date: Date) {
        // Define a query in order to search in the store for tasks related to the identifiers listed below, by date. Empty results are excluded.
        let identifiers = ["doxylamine", "nausea", "kegels", "steps", "heartRate"]
        var query = OCKTaskQuery(for: date)
        query.ids = identifiers
        query.excludesTasksWithNoEvents = true

        // Apply the query to the store manager
        storeManager.store.fetchAnyTasks(query: query, callbackQueue: .main) { result in
            switch result {
            case .failure(let error): print("Error: \(error)")
            case .success(let tasks):
                // If the query was executed without errors, the system continues to process the results, integrating them to the rest of the components.
                // At first, a non-CareKit view is added into the list, using file TipView.swift
                let tipTitle = "Benefits of exercising"
                let tipText = "Learn how activity can promote a healthy pregnancy."

                // Only show the tip view on the current date
                if Calendar.current.isDate(date, inSameDayAs: Date()) {
                    let tipView = TipView()
                    tipView.headerView.titleLabel.text = tipTitle
                    tipView.headerView.detailLabel.text = tipText
                    tipView.imageView.image = UIImage(named: "exercise.jpeg") // "exercise.jpg"
                    listViewController.appendView(tipView, animated: false)
                }

                // Create a card for the kegel task (kegelsCard). Note: since the kegel task is only scheduled every other day, there will be cases where it is not contained in the tasks array returned from the query.
                if let kegelsTask = tasks.first(where: { $0.id == "kegels" }) {
                    let kegelsCard = OCKSimpleTaskViewController(
                        task: kegelsTask,
                        eventQuery: .init(for: date),
                        storeManager: self.storeManager)

                    listViewController.appendViewController(kegelsCard, animated: false)
                }

                // Create a card for the doxylamine task (doxylamineCard) if there are events for it on this day.
                if let doxylamineTask = tasks.first(where: { $0.id == "doxylamine" }) {

                    let doxylamineCard = OCKChecklistTaskViewController(
                        task: doxylamineTask,
                        eventQuery: .init(for: date),
                        storeManager: self.storeManager)

                    listViewController.appendViewController(doxylamineCard, animated: false)
                }

                // Create a card for the nausea task (insightsCard) if there are events for it on this day.
                // Its OCKSchedule was defined to have daily events, so this task should be found in `tasks` every day after the task start date.
                if let nauseaTask = tasks.first(where: { $0.id == "nausea" }) {

                    // dynamic gradient colors to be used in the insights chart
                    let nauseaGradientStart = UIColor { traitCollection -> UIColor in
                        return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.9960784314, green: 0.3725490196, blue: 0.368627451, alpha: 1) : #colorLiteral(red: 0.8627432641, green: 0.2630574384, blue: 0.2592858295, alpha: 1)
                    }
                    let nauseaGradientEnd = UIColor { traitCollection -> UIColor in
                        return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.9960784314, green: 0.4732026144, blue: 0.368627451, alpha: 1) : #colorLiteral(red: 0.8627432641, green: 0.3598620686, blue: 0.2592858295, alpha: 1)
                    }

                    // Create a plot comparing nausea to medication adherence.
                    let nauseaDataSeries = OCKDataSeriesConfiguration(
                        taskID: "nausea",
                        legendTitle: "Nausea",
                        gradientStartColor: nauseaGradientStart,
                        gradientEndColor: nauseaGradientEnd,
                        markerSize: 10,
                        eventAggregator: OCKEventAggregator.countOutcomeValues)

                    let doxylamineDataSeries = OCKDataSeriesConfiguration(
                        taskID: "doxylamine",
                        legendTitle: "Doxylamine",
                        gradientStartColor: .systemGray2,
                        gradientEndColor: .systemGray,
                        markerSize: 10,
                        eventAggregator: OCKEventAggregator.countOutcomeValues)

                    let insightsCard = OCKCartesianChartViewController(
                        plotType: .bar,
                        selectedDate: date,
                        configurations: [nauseaDataSeries, doxylamineDataSeries],
                        storeManager: self.storeManager)

                    insightsCard.chartView.headerView.titleLabel.text = "Nausea & Doxylamine Intake"
                    insightsCard.chartView.headerView.detailLabel.text = "This Week"
                    insightsCard.chartView.headerView.accessibilityLabel = "Nausea & Doxylamine Intake, This Week"
                    listViewController.appendViewController(insightsCard, animated: false)

                    // Also create a card that displays a single event (nauseaCard).
                    // The event query passed into the initializer specifies that only
                    // today's log entries should be displayed by this log task view controller.
                    let nauseaCard = OCKButtonLogTaskViewController(
                        task: nauseaTask,
                        eventQuery: .init(for: date),
                        storeManager: self.storeManager)

                    listViewController.appendViewController(nauseaCard, animated: false)
                }
            }
        }
    }
}

struct MyView: UIViewControllerRepresentable {

    // Reading the store manager from the environment
    @Environment(\.storeManager) private var storeManager

    func makeUIViewController(context: Context) -> CareViewController {
        CareViewController(storeManager: storeManager)
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

    }
}

struct ContentView: View {
    var body: some View {
        MyView()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

KApp.swift

//
//  KApp.swift
//  Shared
//
//  Created by Andrea on 20.10.20.
//

import SwiftUI

@main
struct KApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

TipView.swift

//
//  TipView.swift
//  K
//
//  Created by Andrea on 20.10.20.
//

import UIKit
import CareKit
import CareKitUI

class TipView: OCKView, OCKCardable {

    var cardView: UIView { self }
    let contentView: UIView = OCKView()
    let headerView = OCKHeaderView()
    let imageView = UIImageView()
    var imageHeightConstraint: NSLayoutConstraint!

    private let blurView: UIVisualEffectView = {
        let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.regular)
        return UIVisualEffectView(effect: blurEffect)
    }()

    override init() {
        super.init()
        setup()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setup() {
        headerView.detailLabel.textColor = .secondaryLabel

        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.layer.cornerRadius = layer.cornerRadius

        blurView.clipsToBounds = true
        blurView.layer.cornerRadius = layer.cornerRadius
        blurView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]

        contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        contentView.frame = bounds

        addSubview(contentView)
        contentView.addSubview(imageView)
        contentView.addSubview(blurView)
        contentView.addSubview(headerView)

        imageView.translatesAutoresizingMaskIntoConstraints = false
        blurView.translatesAutoresizingMaskIntoConstraints = false
        headerView.translatesAutoresizingMaskIntoConstraints = false
        imageHeightConstraint = imageView.heightAnchor.constraint(
            equalToConstant: scaledImageHeight(compatibleWith: traitCollection))

        NSLayoutConstraint.activate([
            headerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
            headerView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
            headerView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),

            blurView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            blurView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            blurView.topAnchor.constraint(equalTo: contentView.topAnchor),
            blurView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 16),

            imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
            imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            imageHeightConstraint
        ])
    }

    func scaledImageHeight(compatibleWith traitCollection: UITraitCollection) -> CGFloat {
        return UIFontMetrics.default.scaledValue(for: 200, compatibleWith: traitCollection)
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory {
            imageHeightConstraint.constant = scaledImageHeight(compatibleWith: traitCollection)
        }
    }

    override func styleDidChange() {
        super.styleDidChange()
        let cachedStyle = style()
        enableCardStyling(true, style: cachedStyle)
        directionalLayoutMargins = cachedStyle.dimension.directionalInsets1
    }
}
andreaxricci commented 3 years ago
Screenshot 2020-10-25 at 15 54 07
artniks commented 3 years ago

@andreaxricci Have you added the AppDelegate to your App struct?

import SwiftUI

@main
struct YourApp: App {

    // MARK: - Properties

    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    // MARK: - View

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
andreaxricci commented 3 years ago

@ArtNikSolo Thanks for the hint. I've just added this, but unfortunately the error persists

//
//  KApp.swift
//  Shared
//
//  Created by Andrea on 20.10.20.
//

import SwiftUI

@main
struct KApp: App {

    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
gavirawson-apple commented 3 years ago

It's possible that the issue is related to the use of the environment. Since the store is an object, I'd recommend using environment objects rather than environment values to propagate the store though the view hierarchy.

import SwiftUI

@main
struct KApp: App {

    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appDelegate.synchronizedStoreManager)
        }
    }
}
andreaxricci commented 3 years ago

Thanks for the feedback @gavirawson-apple!

My understanding is that in order to make OCKSynchronizedStoreManager conform to 'ObservableObject', I have to add the @Published property wrapper to the variable "synchronizedStoreManager", otherwise I'd get the error

Instance method 'environmentObject' requires that 'OCKSynchronizedStoreManager' conform to 'ObservableObject'

I tried to modify the code for ContentView.swift as follows, but without success.

//
//  ContentView.swift
//  Shared
//
//  Created by Andrea on 20.10.20.
//

import SwiftUI
import UIKit
import CareKit

//Adding the store manager into the App Delegate
class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
    // Manages synchronization of a CoreData store
    @Published var synchronizedStoreManager: OCKSynchronizedStoreManager = {
        //let store = OCKStore(name: "SampleAppStore")
        let store = OCKStore(name: "SampleAppStore", type: .onDisk)
        store.populateSampleData()
        let manager = OCKSynchronizedStoreManager(wrapping: store)
        return manager
    }()

     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         return true
     }

     func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
         return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
     }
 }

// Extensions for Store Manager
extension EnvironmentValues {
    var storeManager: OCKSynchronizedStoreManager {
        get { self[StoreManagerKey.self] }
        set { self[StoreManagerKey.self] = newValue }
    }
}

private struct StoreManagerKey: EnvironmentKey {
    typealias Value = OCKSynchronizedStoreManager

    // Define the value to be injected into a view's environment if no other value is explicitly set
    static var defaultValue: OCKSynchronizedStoreManager {
        let appDelegate = UIApplication.shared.delegate as! AppDelegate // <-- ERROR HERE: Thread 1: signal SIGABRT
        let manager = appDelegate.synchronizedStoreManager
        return manager
    }
}

[...] same as above

I'm still getting the compile error

Screenshot 2020-10-26 at 20 55 46
gavirawson-apple commented 3 years ago

The conformance to ObservableObject can be defined in an extension:

extension OCKSynchronizedStoreManager: ObservableObject {}

The store manager doesn't have published properties, so it will not trigger SwiftUI view updates. But that will allow you to store the object in a view and pass it through the environment.

andreaxricci commented 3 years ago

thanks. I confirm that solved the compiling error, but unfortunately it didn't fix the initial SIGABRT exception:

Screenshot 2020-10-26 at 21 17 36
gavirawson-apple commented 3 years ago

Based on the screenshot its looks like the store manager is still setup as an environment value. Try removing lines 35-51 and see if that solves it.

artniks commented 3 years ago

@andreaxricci okay I think I've found a solution. As @gavirawson-apple said, firstly conform OCKSynchronizedStoreManager to ObservableObject

extension OCKSynchronizedStoreManager: ObservableObject {}

Then in AppDelegate class make property synchronizedStoreManager static

static var synchronizedStoreManager: OCKSynchronizedStoreManager = {
    //let store = OCKStore(name: "SampleAppStore")
    let store = OCKStore(name: "SampleAppStore", type: .onDisk)
    store.populateSampleData()
    let manager = OCKSynchronizedStoreManager(wrapping: store)
    return manager
}()

In main App struct add synchronizedStoreManager property as EnvironmentObject

@main
struct KApp: App {

    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(AppDelegate.synchronizedStoreManager)
        }
    }
}

And the last change is in MyView instead of reading storeManager from environment add it as EnvironmentObject

struct MyView: UIViewControllerRepresentable {

    // Reading the store manager from the environment
    @EnvironmentObject private var storeManager: OCKSynchronizedStoreManager

    func makeUIViewController(context: Context) -> CareViewController {
        CareViewController(storeManager: storeManager)
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

    }
}

Hope it helps!

andreaxricci commented 3 years ago

Many thanks to both of you, @ArtNikSolo and @gavirawson-apple

much appreciated! I confirm the error is solved.