Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 🎸 Proposal: Decoding JSON in RealityComposerStrategy #47

Merged
merged 5 commits into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Apps/Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
2C124231267B19D800BE28C7 /* Tests.json in Resources */ = {isa = PBXBuildFile; fileRef = 2C124230267B19D800BE28C7 /* Tests.json */; };
2C27448F2644DBAB0088E5DC /* qrImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 2C27448E2644DBAB0088E5DC /* qrImage.png */; };
2C3C67602674006B00B2B243 /* CarEngineRC1.rcproject in Sources */ = {isa = PBXBuildFile; fileRef = 2C3C675F2674006B00B2B243 /* CarEngineRC1.rcproject */; };
2C48248C264DC33300E230E6 /* CarEngineExampleContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48248B264DC33300E230E6 /* CarEngineExampleContentView.swift */; };
Expand All @@ -17,6 +18,7 @@
2C4C71A72643698000B462A9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2C4C71A62643698000B462A9 /* Preview Assets.xcassets */; };
2C4C71B126436A5C00B462A9 /* ARCardsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4C71B026436A5C00B462A9 /* ARCardsContentView.swift */; };
2C4C71B526436ADC00B462A9 /* ARCardsViewBuilderContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4C71B426436ADC00B462A9 /* ARCardsViewBuilderContentView.swift */; };
2C5225B1267BF28A0089A062 /* ARCardsJSONLoadingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5225B0267BF28A0089A062 /* ARCardsJSONLoadingContentView.swift */; };
2C6DDE5E2644CCD60093AA6B /* FioriARKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2C6DDE5D2644CCD60093AA6B /* FioriARKit */; };
2C9371282644D3C3001932D1 /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9371272644D3C3001932D1 /* DownloadsView.swift */; };
2CDD909826437D630076150D /* ExampleCardItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDD909726437D630076150D /* ExampleCardItem.swift */; };
Expand All @@ -25,6 +27,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
2C124230267B19D800BE28C7 /* Tests.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = Tests.json; path = ../../../../../../../Tests.json; sourceTree = "<group>"; };
2C27448E2644DBAB0088E5DC /* qrImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = qrImage.png; sourceTree = "<group>"; };
2C3C675F2674006B00B2B243 /* CarEngineRC1.rcproject */ = {isa = PBXFileReference; lastKnownFileType = file.rcproject; path = CarEngineRC1.rcproject; sourceTree = "<group>"; };
2C48248B264DC33300E230E6 /* CarEngineExampleContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarEngineExampleContentView.swift; sourceTree = "<group>"; };
Expand All @@ -38,6 +41,7 @@
2C4C71B026436A5C00B462A9 /* ARCardsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARCardsContentView.swift; sourceTree = "<group>"; };
2C4C71B426436ADC00B462A9 /* ARCardsViewBuilderContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARCardsViewBuilderContentView.swift; sourceTree = "<group>"; };
2C4C71C026436E2400B462A9 /* cloud-sdk-ios-fioriarkit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "cloud-sdk-ios-fioriarkit"; path = ../..; sourceTree = "<group>"; };
2C5225B0267BF28A0089A062 /* ARCardsJSONLoadingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARCardsJSONLoadingContentView.swift; sourceTree = "<group>"; };
2C9371272644D3C3001932D1 /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = "<group>"; };
2CDD909726437D630076150D /* ExampleCardItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleCardItem.swift; sourceTree = "<group>"; };
2CDD909A26437E7A0076150D /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -79,6 +83,7 @@
2C4C71B426436ADC00B462A9 /* ARCardsViewBuilderContentView.swift */,
2C4C71B026436A5C00B462A9 /* ARCardsContentView.swift */,
2C48248B264DC33300E230E6 /* CarEngineExampleContentView.swift */,
2C5225B0267BF28A0089A062 /* ARCardsJSONLoadingContentView.swift */,
);
path = CardContentViews;
sourceTree = "<group>";
Expand All @@ -97,6 +102,7 @@
children = (
2CDD909A26437E7A0076150D /* Tests.swift */,
2C9371272644D3C3001932D1 /* DownloadsView.swift */,
2C124230267B19D800BE28C7 /* Tests.json */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -228,6 +234,7 @@
buildActionMask = 2147483647;
files = (
2C4C71A72643698000B462A9 /* Preview Assets.xcassets in Resources */,
2C124231267B19D800BE28C7 /* Tests.json in Resources */,
2C4C71A42643698000B462A9 /* Assets.xcassets in Resources */,
2C27448F2644DBAB0088E5DC /* qrImage.png in Resources */,
);
Expand All @@ -243,6 +250,7 @@
2C3C67602674006B00B2B243 /* CarEngineRC1.rcproject in Sources */,
2C4C71A22643697F00B462A9 /* ContentView.swift in Sources */,
2C4C71B126436A5C00B462A9 /* ARCardsContentView.swift in Sources */,
2C5225B1267BF28A0089A062 /* ARCardsJSONLoadingContentView.swift in Sources */,
2C48248C264DC33300E230E6 /* CarEngineExampleContentView.swift in Sources */,
2C4C71B526436ADC00B462A9 /* ARCardsViewBuilderContentView.swift in Sources */,
2C4C71A02643697F00B462A9 /* ExamplesApp.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct ARCardsDefaultContentView: View {
func loadInitialData() {
let cardItems = Tests.carEngineCardItems
guard let anchorImage = UIImage(named: "qrImage") else { return }
let strategy = RealityComposerStrategy(cardContents: cardItems, anchorImage: anchorImage, physicalWidth: 0.1, rcFile: "ExampleRC", rcScene: "ExampleScene")
let strategy = RCProjectStrategy(cardContents: cardItems, anchorImage: anchorImage, physicalWidth: 0.1, rcFile: "ExampleRC", rcScene: "ExampleScene")
arModel.load(loadingStrategy: strategy)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// ARCardsDefaultContentView.swift
// Examples
//
// Created by O'Brien, Patrick on 5/5/21.
//

import FioriARKit
import SwiftUI

struct ARCardsJSONLoadingContentView: View {
@StateObject var arModel = ARAnnotationViewModel<DefaultCardItem>()

var body: some View {
SingleImageARCardView(arModel: arModel,
image: Image("qrImage"),
cardAction: { id in
// set the card action for id corresponding to the CardItemModel
print(id)
})
.onAppear(perform: loadInitialData)
}

func loadInitialData() {
guard let anchorImage = UIImage(named: "qrImage"), let url = Bundle.main.url(forResource: "Tests", withExtension: "json") else { return }

do {
let jsonData = try Data(contentsOf: url)
let strategy = try RCProjectStrategy(jsonData: jsonData, anchorImage: anchorImage, physicalWidth: 0.1, rcFile: "ExampleRC", rcScene: "ExampleScene")
arModel.load(loadingStrategy: strategy)
} catch {
print(error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ struct ARCardsViewBuilderContentView: View {
func loadInitialData() {
let cardItems = Tests.carEngineCardItems
guard let anchorImage = UIImage(named: "qrImage") else { return }
let strategy = RealityComposerStrategy(cardContents: cardItems, anchorImage: anchorImage, physicalWidth: 0.1, rcFile: "ExampleRC", rcScene: "ExampleScene")
let strategy = RCProjectStrategy(cardContents: cardItems, anchorImage: anchorImage, physicalWidth: 0.1, rcFile: "ExampleRC", rcScene: "ExampleScene")
arModel.load(loadingStrategy: strategy)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct CarEngineExampleContentView: View {
func loadData() {
let cardItems = Tests.carEngineCardItems
guard let anchorImage = UIImage(named: "carSticker") else { return }
let strategy = RealityComposerStrategy(cardContents: cardItems, anchorImage: anchorImage, physicalWidth: 0.15, rcFile: "CarEngineRC1", rcScene: "EngineScene")
let strategy = RCProjectStrategy(cardContents: cardItems, anchorImage: anchorImage, physicalWidth: 0.15, rcFile: "CarEngineRC1", rcScene: "EngineScene")
arModel.load(loadingStrategy: strategy)
}
}
4 changes: 4 additions & 0 deletions Apps/Examples/Examples/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ struct ContentView: View {
Text("2016 Engine Example")
}

NavigationLink(destination: ARCardsJSONLoadingContentView()) {
Text("JSON Decoding Example")
}

NavigationLink(destination: DownloadsView()) {
Text("Download Image Anchors")
}
Expand Down
50 changes: 50 additions & 0 deletions Sources/FioriARKit/ARCards/Models/CardItemModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,53 @@ public protocol ActionTextComponent {
public protocol IconComponent {
var icon_: Image? { get }
}

internal struct CodableCardItem: Codable {
pgobriensap marked this conversation as resolved.
Show resolved Hide resolved
var id: String
var title: String
var descriptionText: String?
var detailImage: Data?
var actionText: String?
var icon: String?
}

public struct DefaultCardItem: CardItemModel {
pgobriensap marked this conversation as resolved.
Show resolved Hide resolved
public var id: String
public var title_: String
public var descriptionText_: String?
public var detailImage_: Image?
public var actionText_: String?
public var icon_: Image?

private enum CodingKeys: String, CodingKey {
case id
case title_
case descriptionText_
case detailImage_
case actionText_
case icon_
}
}

extension DefaultCardItem: Decodable {
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)

self.id = try values.decode(String.self, forKey: .id)
self.title_ = try values.decode(String.self, forKey: .title_)
self.descriptionText_ = try values.decode(String?.self, forKey: .descriptionText_)
let imageData: Data? = try values.decode(Data?.self, forKey: .detailImage_)
var image: Image?
if let unwrappedImageData = imageData {
if let uiImage = UIImage(data: unwrappedImageData) {
image = Image(uiImage: uiImage)
} else {
throw LoadingStrategyError.base64DecodingError
}
}
self.detailImage_ = image
self.actionText_ = try values.decode(String?.self, forKey: .actionText_)
let iconString: String? = try values.decode(String?.self, forKey: .icon_)
self.icon_ = iconString != nil ? Image(systemName: iconString!) : nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import SwiftUI
/// This strategy wraps the anchors that represents these locations with the CardItemModels that they correspond to in a ScreenAnnotation struct for a single source of truth.
/// Loading the data into the ARAnnotationViewModel should be done in the onAppear method.
///
/// If an Object Anchor is used the anchorImage and physicalWidth can be set to nil and are ignored
///
/// - Parameters:
/// - cardContents: An array of **CardItem : `CardItemModel`** which represent what will be displayed in the default CardView
/// - anchorImage: Image to be converted to ARReferenceImage and added to ARConfiguration for discovery, can be nil if detecting an object Anchor
Expand All @@ -28,27 +30,57 @@ import SwiftUI
/// let strategy = RealityComposerStrategy(cardContents: cardItems, anchorImage: anchorImage, rcFile: "ExampleRC", rcScene: "ExampleScene")
/// arModel.load(loadingStrategy: strategy)
/// ```
public struct RealityComposerStrategy<CardItem: CardItemModel>: AnnotationLoadingStrategy where CardItem.ID: LosslessStringConvertible {

public struct RCProjectStrategy<CardItem: CardItemModel>: AnnotationLoadingStrategy where CardItem.ID: LosslessStringConvertible {
public var cardContents: [CardItem]
public var anchorImage: UIImage?
public var physicalWidth: CGFloat?
public var rcFile: String
public var rcScene: String

/// Constructor for loading annotations using an Image as an anchor with a Reality Composer scene
public init(cardContents: [CardItem], anchorImage: UIImage, physicalWidth: CGFloat, rcFile: String, rcScene: String) {
public init(cardContents: [CardItem], anchorImage: UIImage? = nil, physicalWidth: CGFloat? = nil, rcFile: String, rcScene: String) {
self.cardContents = cardContents
self.anchorImage = anchorImage
self.physicalWidth = physicalWidth
self.rcFile = rcFile
self.rcScene = rcScene
}

/// Constructor for loading annotations using an Object as an anchor with a Reality Composer scene
public init(cardContents: [CardItem], rcFile: String, rcScene: String) {
self.cardContents = cardContents
self.anchorImage = nil
self.physicalWidth = nil

/**
Constructor for loading annotations using Data from a JSON Array
JSON key/value::
"id": String,
"title": String,
"descriptionText": String?,
"detailImage": Data?, // base64 encoding of Image
"actionText": String?,
"icon": String? // systemName of SFSymbol

Example:
[
{
"id": "WasherFluid",
"title": "Recommended Washer Fluid",
"descriptionText": "Rain X",
"detailImage": null,
"actionText": null,
"icon": null
},
{
"id": "Coolant",
"title": "Genuine Coolant",
"descriptionText": "Price: 20.99",
"detailImage": "iVBORw0KGgoAAAANSUhE...",
"actionText": "Order",
"icon": "cart.fill"
}
]
*/
public init(jsonData: Data, anchorImage: UIImage? = nil, physicalWidth: CGFloat? = nil, rcFile: String, rcScene: String) throws where CardItem == DefaultCardItem {
self.cardContents = try JSONDecoder().decode([DefaultCardItem].self, from: jsonData)
self.anchorImage = anchorImage
self.physicalWidth = physicalWidth
self.rcFile = rcFile
self.rcScene = rcScene
}
Expand All @@ -58,7 +90,7 @@ public struct RealityComposerStrategy<CardItem: CardItemModel>: AnnotationLoadin
var annotations = [ScreenAnnotation<CardItem>]()

guard let scene = try? RCScanner.loadScene(rcFileName: rcFile, sceneName: rcScene) else {
throw LoadingStrategyError.sceneLoadingFailed
throw LoadingStrategyError.sceneLoadingFailedError
}

// An image should use world tracking so we set the configuration to prevent automatic switching to Image Tracking
Expand All @@ -72,12 +104,12 @@ public struct RealityComposerStrategy<CardItem: CardItemModel>: AnnotationLoadin
manager.setAutomaticConfiguration()
manager.addAnchor(for: scene)
default:
throw LoadingStrategyError.anchorTypeNotSupported
throw LoadingStrategyError.anchorTypeNotSupportedError
}

for cardItem in self.cardContents {
guard let internalEntity = scene.findEntity(named: String(cardItem.id)) else {
throw LoadingStrategyError.entityNotFound(cardItem.id)
throw LoadingStrategyError.entityNotFoundError(cardItem.id)
}
let annotation = ScreenAnnotation(card: cardItem)
annotation.setInternalEntity(with: internalEntity)
Expand All @@ -88,8 +120,10 @@ public struct RealityComposerStrategy<CardItem: CardItemModel>: AnnotationLoadin
}
}

private enum LoadingStrategyError: Error {
case anchorTypeNotSupported
case entityNotFound(LosslessStringConvertible)
case sceneLoadingFailed
internal enum LoadingStrategyError: Error {
case anchorTypeNotSupportedError
case entityNotFoundError(LosslessStringConvertible)
case sceneLoadingFailedError
case jsonDecodingError
case base64DecodingError
}