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 all 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 @@ -17,6 +17,8 @@
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 */; };
2C5225B7267CF7980089A062 /* Tests.json in Resources */ = {isa = PBXBuildFile; fileRef = 2C5225B6267CF7980089A062 /* Tests.json */; };
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 @@ -38,6 +40,8 @@
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>"; };
2C5225B6267CF7980089A062 /* Tests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Tests.json; 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 @@ -95,6 +100,7 @@
2C2D2BCA2645BF5000A1E1B1 /* Utils */ = {
isa = PBXGroup;
children = (
2C5225B6267CF7980089A062 /* Tests.json */,
2CDD909A26437E7A0076150D /* Tests.swift */,
2C9371272644D3C3001932D1 /* DownloadsView.swift */,
);
Expand Down Expand Up @@ -228,6 +234,7 @@
buildActionMask = 2147483647;
files = (
2C4C71A72643698000B462A9 /* Preview Assets.xcassets in Resources */,
2C5225B7267CF7980089A062 /* 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
@@ -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<DecodableCardItem>()

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 RealityComposerStrategy(jsonData: jsonData, anchorImage: anchorImage, physicalWidth: 0.1, rcFile: "ExampleRC", rcScene: "ExampleScene")
arModel.load(loadingStrategy: strategy)
} catch {
print(error)
}
}
}
50 changes: 50 additions & 0 deletions Apps/Examples/Examples/ARCards/Utils/Tests.json

Large diffs are not rendered by default.

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
41 changes: 41 additions & 0 deletions Sources/FioriARKit/ARCards/Models/CardItemModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,44 @@ public protocol ActionTextComponent {
public protocol IconComponent {
var icon_: Image? { get }
}

public struct DecodableCardItem: CardItemModel {
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 DecodableCardItem: 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,6 +30,7 @@ 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 var cardContents: [CardItem]
public var anchorImage: UIImage?
Expand All @@ -36,19 +39,48 @@ public struct RealityComposerStrategy<CardItem: CardItemModel>: AnnotationLoadin
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 == DecodableCardItem {
self.cardContents = try JSONDecoder().decode([DecodableCardItem].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
}