Skip to content

Commit

Permalink
feat: 🎸 Proposal: Decoding JSON in RealityComposerStrategy (#47)
Browse files Browse the repository at this point in the history
 feat: 🎸 Support for passing data as JSON in Reality Composer Strategy
  • Loading branch information
pgobriensap authored Jun 21, 2021
1 parent 9a16ae5 commit 6c14176
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 14 deletions.
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
}

0 comments on commit 6c14176

Please sign in to comment.