A powerful SwiftUI Architecture that merges Redux to the functional world of Swift. While bringing powerful workflows to streamline CoreML/Metal/IPFS usage in the Apple ecosystem.

Overview

GraniteUI - v0.0 - WIP

Travis CI Build Swift 5.3 xcode-version License

A powerful SwiftUI Architecture that merges Redux event handling and state management with functional programming. While bringing powerful workflows to streamline CoreML/Metal work and to interact with ground-breaking services like IPFS.

The Granite architecture provides:

  • Testability and Isolation. Routing, business, view logic, have independant classes/protocols to be inherited. Each component is positioned to be decoupled and independant from eachother.
  • Tooling for developer productivity. Granite comes with pre-built components to streamline Metal, CoreML integration as well as UserDefault-backed local storage solutions.
  • View Library. Granite comes with a comprehensive view library that can speed up development, offering thought out button templates and other UX solutions.
  • IPFS capability, any media type you handle with Granite's framework can be pinned and distributed from IPFS with ease. Based on: https://github.com/ipfs-shipyard/swift-ipfs-http-client

High Level Overview

The Docs folder has usage examples for each file. Some are still WIP.

  • GraniteComponent (80%)
    • GraniteCommand (50%)
      • GraniteCenter (50%)
        • GraniteState (90%)
      • GraniteExpedition (Reducers) (90%)
  • GraniteRelay (80%)
    • GraniteService (50%)
      • GraniteCenter (50%)
        • GraniteState (90%)
      • GraniteExpedition (Reducers) (90%)
  • GraniteEvent (80%)
    • GraniteBeam (80%)
    • GraniteBeamType (80%)
  • GraniteML (25%)
  • GraniteIPFS (25%)
  • GraniteMetal/MarbleKit (0%)
  • Types (3%)
    • GraniteConnection (0%)
      • GraniteSatellite (0%)
      • GraniteAdAstra (10%)
    • GraniteDock (0%)
    • GraniteLaunch (0%)

Documentation is a work in progress. Reference the above for progress. It is to show the main types one may experience when using the architecture. An example Application is provided to speed-up bootstrapping an application with Granite until Documentation is updated further.

High Level Overview

Every SwiftUI View is a GraniteComponent with business logic handled in reducer like design standards (Expeditions) with distinct state files to hold mutable data.

GraniteRelays are modular Services lightweight components that can handle non UI dependant actions updating data globally to any components hosting them. The GraniteCenter distinctly defines your rules of Services which are known as GraniteRelays allowed to be accessed. GraniteCenters also allow the definition of Reducers and Dependencies the component requires. It is the cornerstone of a GraniteComponent.

GraniteComponent & GraniteRelay

GraniteComponent Data Path

GraniteComponent

import GraniteUI

public struct MainComponent: GraniteComponent {
    @ObservedObject
    public var command: GraniteCommand<MainCenter, MainState> = .init()
    
    public init() {}
    
    public var body: some View {}
}

Base GraniteComponent classes resemble SwiftUI View classes, but have the ability to modularize object states, events, and business logic. Let the View class only be UI/UX, divide responsibility of code, to make it easier to develop complex experiences.

@ObservedObject
public var command: GraniteCommand<MainCenter, MainState> = .init()

The ObservedObject is crucial for a component to see it's relevant State file and Center file. Centers handle the pre-loading and linking of reducers, services and constants a View class would need to access, while helping the State be available for Reducers to mutate and be read from a View file for UX based decisions on state data.

GraniteRelay

public struct MarqueRelay: GraniteRelay {
    @ObservedObject
    public var command: GraniteService<MarqueCenter, MarqueState> = .init()
    
    public init() {}
    
    public func setup() {}
    
    public func clean() {}
    
    public func cancel() {}
}

GraniteRelays resemble microservices floating specialized logic that can attach to host components to deliver specific tasks.

GraniteRelay Event Behavior

In a functional datapath, re-draws are expensive when data updates within Components. We can control this with distinct enum flags when sending updates to a relay's or a component's own expeditions.

In an app that is displaying realtime financial data of stocks or securities. It's cheaper and more effecient to have a singular service to contact a back-end service to retrieve updates once. The challenge to update all components in an application to handle these updated changes can be handled via a GraniteRelay and broadcasting.

GraniteCenter & GraniteState

public enum ListenStage {
    case listening
    case none
}

public class ListenState: GraniteState {
    var exhibitionStage: ExhibitionStage
    var stage: ListenStage = .none
    var intent: ExhibitionIntent
    var wantsToListen: Bool = false
    
    public init(stage: ExhibitionStage, intent: ExhibitionIntent) {
        self.exhibitionStage = stage
        self.intent = intent
    }
    
    public required init() {
        self.exhibitionStage = .none
        self.intent = .none
    }
}

public class ListenCenter: GraniteCenter<ListenState> {
    @GraniteDependency
    var envDependency: EnvironmentDependency
    
    public override var expeditions: [GraniteBaseExpedition] {
        [
            ListenStageExpedition.Discovery(),
            ListenPermissionsExpedition.Discovery(),
            RadioExpedition.Discovery(),
            StopExpedition.Discovery()
        ]
    }
    
    public override var links: [GraniteLink] {
        [
            .onAppear(ListenEvents.Prepare(), .dependant)
        ]
    }
    
    public override var behavior: GraniteEventBehavior {
        .broadcastable
    }
}

A basic state file contains both the Center and State classes, one could seperate them, but they are together for maintaining easy context of defined expeditions in a Center and state variables required within each.

This file is referencing a component called ListenComponent a view that handles a music player.

@GraniteDependency
var envDependency: EnvironmentDependency

We will dive into this later in the README, but defining a usable dependency for easy access of shareable mutable data of multiple components. Can be updated directly from a view with a specialized setter, with a preference to update within expeditions exclusively.

public override var expeditions: [GraniteBaseExpedition] {
    [
        ListenStageExpedition.Discovery(),
        ListenPermissionsExpedition.Discovery(),
        RadioExpedition.Discovery(),
        StopExpedition.Discovery()
    ]
}

Expeditions are Granite's Reducers, they need to become executable .Discovery() does this making them available in the frameworks' back-end when being searched for. An associatedtype of a relevant Event is used to link a expedition with it's calling function. We'll get into this in the GraniteExpedition section of the README.

public override var links: [GraniteLink] {
    [
        .onAppear(ListenEvents.Prepare(), .dependant)
    ]
}

Bringing viewDidAppear's handling in UIKit to SwiftUI. Fire events on a components appear, setting flags like dependant which runs the event only once @GraniteDependency wrapped deps are linked to the component or .always firing the event every time a component loads/re-draws.

public override var behavior: GraniteEventBehavior {
    .broadcastable
}

Components can use GraniteEventBehaviors as well. When referencing the last visual, a broadcastable allows this component to be searched for expeditions to execute based on a parent or this component's services update. Other options like .quiet (default) allow it to be skipped to improve performance.

GraniteEvent

GraniteButtonComponent(state: .init("♫_le_Verre",
	                            textColor: Brand.Colors.white,
	                            colors: [Brand.Colors.yellow, Brand.Colors.purple],
	                            padding: .init(Brand.Padding.medium,
	                                           Brand.Padding.small,
	                                           Brand.Padding.medium,
	                                           Brand.Padding.small),
	                            action: {
	                                sendEvent(ListenEvents.Radio.Begin()) // <---
	                            }))

Events are easy to send from a View's action handler in a component.

struct ListenEvents {
    public struct Listen: GraniteEvent {
        public struct Permissions: GraniteEvent {
            public var behavior: GraniteEventBehavior {
                .quiet
            }
        }
    }
}

Defined in a seperate file. Events can come with pre-defined behaviors without specifying a GraniteBeamType in the sendEvent action.

sendEvent(ListenEvents.Radio.Begin(), haptic: .light)

Easily setup haptic responses with an additional parameter.

public enum GraniteHaptic {
    case light
    case none // default
}

Currently supported haptic types, more to be added.

GraniteExpedition

import GraniteUI
import Combine
import MarbleKit
import Foundation

struct ListenPermissionsExpedition: GraniteExpedition {
    typealias ExpeditionEvent = ListenEvents.Listen.Permissions
    typealias ExpeditionState = ListenState
    
    func reduce(
        event: ExpeditionEvent,
        state: ExpeditionState,
        connection: GraniteConnection,
        publisher: inout AnyPublisher<GraniteEvent, Never>) {
        
        if GlobalDefaults.Permissions.Listen.canListen {
            connection.request(ExhibitionEvents.Glass.Listen(), .contact)
            state.wantsToListen = false
            state.stage = .listening
        } else {
            #if os(macOS)
            AVCaptureDevice.requestAccess(for: .audio) { granted in
                if granted {
                    storage.update(GlobalDefaults.Permissions.Listen.on)
                } else {
                    storage.update(GlobalDefaults.Permissions.Listen.off)
                }
            }
            #else
            let audioSession = AVAudioSession.sharedInstance()
            
            audioSession.requestRecordPermission { granted in
                if granted {
                    storage.update(GlobalDefaults.Permissions.Listen.on)
                } else {
                    storage.update(GlobalDefaults.Permissions.Listen.off)
                }
            }
            #endif
        }
    }
}

This is Granite's reducers. An event is fired to check and request permissions for a devices mic.

typealias ExpeditionEvent = ListenEvents.Listen.Permissions
typealias ExpeditionState = ListenState

These are two key types that need to match the component that linked this expedition from it's center:

public override var expeditions: [GraniteBaseExpedition] {
    [
        ListenStageExpedition.Discovery(),
        ListenPermissionsExpedition.Discovery(), // <---
        RadioExpedition.Discovery(),
        StopExpedition.Discovery()
    ]
}

GraniteDependency

public class ListenCenter: GraniteCenter<ListenState> {
    @GraniteDependency
    var envDependency: EnvironmentDependency
}

GraniteDependencies are powerful static files that can hold mutable data in one place while communicating data to other components. Updated and retrieved from Expeditions they can maintain important data like profile/user custom types that may affect UI/UX behaviors across an applications' diverse component tree.

class EnvironmentDependency: GraniteDependable {
    var home: Home = .init()
    
    var user: User = .init()
    
    var exhibition: Exhibition = .init()
    
    var envSettings: EnvironmentStyle.Settings = .init()
}

A dependency is simply a class object that inherits from GraniteDependable. Building a dependency with a component is as easy as 2 lines within it's center.

@GraniteDependency
var envDependency: EnvironmentDependency

No extra steps or initialization required.

Retrieving Data from a Dep in a Expedition

func reduce(event: ExpeditionEvent,
	        state: ExpeditionState,
	        connection: GraniteConnection,
	        publisher: inout AnyPublisher<GraniteEvent, Never>) {
        
    guard let envSettings = connection.retrieve(\EnvironmentDependency.envSettings) {
        return
    }
    
}

Updating Data in a Dep in a Expedition

func reduce(event: ExpeditionEvent,
	        state: ExpeditionState,
	        connection: GraniteConnection,
	        publisher: inout AnyPublisher<GraniteEvent, Never>) {
        
    connection.update(\RouterDependency.authState, value: .authenticated)
    
}

Updating Dep Behaviors

connection.update(\RouterDependency.authState, value: .authenticated, .here)

Dependencies can be updated with targeted re-draws, in case a UI element's state or UX is dependant on a dependency's variable. .here only redraws the called component, child components will not re-draw to maintain performance.

connection.update(\RouterDependency.authState, value: .authenticated, .target(ChildCenter.self))

One can target a child component, preventing any other components in a chain from re-drawing maintaining performance.

public enum GraniteLander {
    case target(GraniteAdAstra.Type)
    case home
    case quiet
    case none
}

The type used is called GraniteLander similar to GraniteBeamType and GraniteEventBehavior. GraniteAdAstra is a base generic protocol GraniteCenters inherit.

GraniteAttach & GraniteListen

GraniteAttach & Listen Datapath

public struct ExhibitionComponent: GraniteComponent {
    @ObservedObject
    public var command: GraniteCommand<ExhibitionCenter, ExhibitionState> = .init()
    
    public init() {}
    
    public var body: some View {
        InstallationComponent(state: .init(state.installations, isShowcase: true))
        	.attach(to: command) // <---
        	.listen(to: command) // <---
    }
}

Attach

Attaching child components to a parent's GraniteCommand allows a child to call a parent's hosted events and thus, its expeditions.

struct GenerateSiteResultMarqueExpedition: GraniteExpedition {
    typealias ExpeditionEvent = MarqueEvents.Pin.Site.Result
    typealias ExpeditionState = LaMarqueState

    func reduce(
        event: ExpeditionEvent,
        state: ExpeditionState,
        connection: GraniteConnection,
        publisher: inout AnyPublisher<GraniteEvent, Never>) {
        
        connection.request(ExhibitionEvents.Get(), .contact) // <---
    }
}

When sending an event from a Component's view's action handler or requesting an event from an Expedition a .contact GraniteBeamType must be provided for the event handling to channel this to it's relevant parent component.

Listen

Listening allows a child's hosted events/expeditions to be called from a parent.

struct ShowcaseInstallationExpedition: GraniteExpedition {
    typealias ExpeditionEvent = InstallationEvents.Advance
    typealias ExpeditionState = InstallationState
    
    func reduce(
        event: ExpeditionEvent,
        state: ExpeditionState,
        connection: GraniteConnection,
        publisher: inout AnyPublisher<GraniteEvent, Never>) {
        guard state.installations.isNotEmpty else { return }
        
        let installation = state.installations[state.showcaseIndex]
        state.installation = installation
        state.originalImage = installation.originalImage
        
        let nextIndex = state.showcaseIndex + 1
        if nextIndex < state.installations.count {
            state.showcaseIndex = nextIndex
        } else {
            state.showcaseIndex = 0
        }
        
        connection.hear(event: CanvasEvents.Prepare.init(installation: installation)) // <---
    }
}

One must call connection.hear for the event handling to recognize a child should respond to this call.

GraniteML

let model: GraniteML<MLMultiArray>? = GraniteML<MLMultiArray>(MiDaS.self, input: .buffer)
//let model: GraniteML<OutputType>?

Machine learning models can be easily loaded and used right away.

guard let depthMap = model.predict(buffer) else {
    return
}

That's literally it! Obviously, pre-processing inputs and post-processing is reliant on the model you are using. Granted you are aware of MLMultiArray and other types of features. Look forward to more in-depth guides to ML when the documentation is further worked on. The plan is to help on-board skill levels of all kinds to utilize CoreML throughout their creations.

GraniteIPFS

let client: GraniteIPFS?
    
public init() {
    client = try? GraniteIPFS.init(host: "ipfs.infura.io", port: 5001, ssl: true)
}

Granite is bundled with the necessary tools to utilize IPFS right away to store and share data on the distributed web. Also allowing one to pin content.

Adding data to IPFS

if let data = result.data.pngData() {
    do {
        try state.service.client?.add(data) { nodes in // Adding data to IPFS
            connection.request(MarqueEvents.Pin(nodes: nodes, encodedData: data)) // Sending a PIN request
        }
    } catch let error {
        GraniteLogger.info("error adding new image:\n\(error)", .expedition, focus: true)
    }
}

Convert any type of object into a Data type to prepare for adding.

Pinning data to IPFS

do {
	if let addHash = event.nodes.first?.hash { // the `nodes` from the closure of the previous code example
		try state.service.client?.pin.add(addHash) { pinHash in
			let hash = b58String(pinHash) // IPFS public hash gateway for the pinned content
		}
	}
} catch let error {
    GraniteLogger.info("error pinning:\n\(error)", .expedition, focus: true)
}
                

In the future, Granite will have extensions such as let hash = image.pin() to handle all of the above in 1 line (aside from client init).

Q/A, Suggestions, Want to contribute?

discord: https://discord.gg/fyXRsDEd

You might also like...
Text-cli - Command line tool for extracting text from images using Apple's Vision framework
Text-cli - Command line tool for extracting text from images using Apple's Vision framework

text-cli Command line tool for extracting text from images using Apple's Vision

Coffee App Splash Screen made with SwiftUI.
Coffee App Splash Screen made with SwiftUI.

SplashScreen-CoffeeApp Coffee App Splash Screen made with SwiftUI. SplashScreen - Code struct ContentView: View { @State var splashScreen = true

Slot-machine - SwiftUI - iOS App Slot Machine game
Slot-machine - SwiftUI - iOS App Slot Machine game

Slot Machine SwiftUI Masterclass project iPhone, iPad and Mac (with Catalyst framework) Complex interface Extensions and custom View Modifiers Input t

C-19-StatusSwiftUI - Covid-19 Status App made with SwiftUI
C-19-StatusSwiftUI - Covid-19 Status App made with SwiftUI

C-19-StatusSwiftUI C-19 Status App made with SwiftUI . First Creator of App - th

Tool to convert SVG to SwiftUI's Shape structure.
Tool to convert SVG to SwiftUI's Shape structure.

SVG to SwiftUI Converter Tool to convert SVG to SwiftUI's Shape structure. This approach is much more memory efficient than introducing a SVG library

MarinersCompassSwiftUI - MarinersCompass Animation made with SwiftUI
MarinersCompassSwiftUI - MarinersCompass Animation made with SwiftUI

MarinersCompassSwiftUI MarinersCompass Animation made with SwiftUI.

RainbowGaugeSwiftUI - Rainbow Gauge made with SwiftUI
RainbowGaugeSwiftUI - Rainbow Gauge made with SwiftUI

RainbowGaugeSwiftUI Rainbow Gauge made with SwiftUI.

Spiral is a SwiftUI shape for macOS, iOS and watchOS.
Spiral is a SwiftUI shape for macOS, iOS and watchOS.

Spiral is a SwiftUI shape for macOS, iOS and watchOS. A spiral is a component that includes a point generator, Shape and View of the spiral. The point

Accelerated tensor operations and dynamic neural networks based on reverse mode automatic differentiation for every device that can run Swift - from watchOS to Linux
Accelerated tensor operations and dynamic neural networks based on reverse mode automatic differentiation for every device that can run Swift - from watchOS to Linux

DL4S provides a high-level API for many accelerated operations common in neural networks and deep learning. It furthermore has automatic differentiati

Owner
Kala
Kala
A Swift library for creating and exporting CoreML Models in Swift

SwiftCoreMLTools A Swift Library for creating CoreML models in Swift. Work in progress This library expose a (function builder based) DSL as well as a

Jacopo Mangiavacchi 140 Dec 5, 2022
Resource monitor - A flutter plugin for Android and IOS to monitor CPU and RAM usage of device.

resource_monitor A flutter plugin for Android and IOS to monitor CPU and RAM usage of device. TODO Implement Android Side of this plugin. Add listener

Skandar Munir 1 Nov 11, 2022
WhatPet - A basic app that classifies images of dogs, cats and rabbits using CoreML

WhatPet ✓ A basic app that classifies images of dogs, cats and rabbits using Cor

Micaella Morales 0 Jan 6, 2022
CoreMLSample - CoreML Example for in app model and download model

CoreMLSample Sample for CoreML This project is CoreML Example for in app model a

Kim Seonghun 2 Aug 31, 2022
Hand-gesture recognition on iOS app using CoreML

GestureAI-CoreML-iOS Hand-gesture recognizer using CoreML Demo Screenshots This app is using RNN(Recurrent Neural network) with CoreML on iOS11. The m

null 151 Nov 2, 2022
Tiny YOLO for iOS implemented using CoreML but also using the new MPS graph API.

YOLO with Core ML and MPSNNGraph This is the source code for my blog post YOLO: Core ML versus MPSNNGraph. YOLO is an object detection network. It can

Matthijs Hollemans 900 Dec 31, 2022
An example of CoreML using a pre-trained VGG16 model

CoreMLExample In this example we use AVFoundation to continuously get image data from the back camera, and try to detect the dominant objects present

Aleph Retamal 34 Apr 22, 2022
A Swift deep learning library with Accelerate and Metal support.

Serrano Aiming to offering popular and cutting edge techs in deep learning area on iOS devices, Serrano is developed as a tool for developers & resear

pcpLiu 51 Nov 17, 2022
Easily craft fast Neural Networks on iOS! Use TensorFlow models. Metal under the hood.

Bender Bender is an abstraction layer over MetalPerformanceShaders useful for working with neural networks. Contents Introduction Why did we need Bend

xmartlabs 1.7k Dec 24, 2022
Sample code for Core ML using ResNet50 provided by Apple and a custom model generated by coremltools.

CoreML-samples This is the sample code for Core ML using ResNet50 provided by Apple. ResNet50 can categorize the input image to 1000 pre-trained categ

Yuta Akizuki 39 Nov 11, 2022