Skip to content

Commit

Permalink
New preferences (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
noahsmartin authored Oct 13, 2023
1 parent 382a658 commit 8ff68eb
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 95 deletions.
1 change: 1 addition & 0 deletions DemoApp/DemoApp/TestViews/ExpandingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import SwiftUI
import SnapshotPreferences

struct ExpandingView: View {
var body: some View {
Expand Down
9 changes: 8 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ let package = Package(
.library(
name: "SnapshottingTests",
targets: ["SnapshottingTests"]),
// Link the main app to this target to use custom snapshot settings
// This lib does not get inserted when running tests to avoid
// duplicate symbols.
.library(
name: "SnapshotPreferences",
targets: ["SnapshotPreferences"]),
// Core functionality for snapshotting exported from the internal package
.library(
name: "SnapshotPreviewsCore",
Expand All @@ -36,11 +42,12 @@ let package = Package(
.target(name: "SnapshottingTests"),
// Core functionality
.target(name: "SnapshotPreviewsCore"),
.target(name: "SnapshotPreferences", dependencies: ["SnapshotPreviewsCore"]),
// Inserted dylib
.target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]),
// Swift code in the inserted dylib
.target(name: "SnapshottingSwift", dependencies: ["SnapshotPreviewsCore", .product(name: "FlyingFox", package: "FlyingFox")]),
.target(name: "PreviewGallery", dependencies: ["SnapshotPreviewsCore"]),
.target(name: "PreviewGallery", dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"]),
.testTarget(
name: "SnapshotPreviewsTests",
dependencies: ["SnapshotPreviewsCore"]),
Expand Down
48 changes: 48 additions & 0 deletions Sources/SnapshotPreferences/EmergeModifierFinder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// EmergeModifierFinder.swift
//
//
// Created by Noah Martin on 10/6/23.
//

import Foundation
import SwiftUI
import SnapshotPreviewsCore

// Classes in this file get compiled to an app that use any of the custom preview preferences.
// The inserted test runner code finds these classes through ObjC runtime functions (NSClassFromString)
// and Swift reflection (Mirror).

@objc(EmergeModifierState)
class EmergeModifierState: NSObject {

@objc
static let shared = EmergeModifierState()

func reset() {
expansionPreference = nil
renderingMode = nil
precision = nil
}

var expansionPreference: Bool?
var renderingMode: EmergeRenderingMode.RawValue?
var precision: Float?
}

@objc(EmergeModifierFinder)
class EmergeModifierFinder: NSObject {
let finder: (any View) -> (any View) = { view in
EmergeModifierState.shared.reset()
return view
.onPreferenceChange(ExpansionPreferenceKey.self, perform: { value in
EmergeModifierState.shared.expansionPreference = value
})
.onPreferenceChange(RenderingModePreferenceKey.self, perform: { value in
EmergeModifierState.shared.renderingMode = value
})
.onPreferenceChange(PrecisionPreferenceKey.self, perform: { value in
EmergeModifierState.shared.precision = value
})
}
}
25 changes: 25 additions & 0 deletions Sources/SnapshotPreferences/ExpansionPreference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// ExpansionPreference.swift
//
//
// Created by Noah Martin on 9/7/23.
//

import Foundation
import SwiftUI

struct ExpansionPreferenceKey: PreferenceKey {
static func reduce(value: inout Bool?, nextValue: () -> Bool?) {
if value == nil {
value = nextValue()
}
}

static var defaultValue: Bool? = nil
}

extension View {
public func emergeExpansion(_ enabled: Bool?) -> some View {
preference(key: ExpansionPreferenceKey.self, value: enabled)
}
}
23 changes: 23 additions & 0 deletions Sources/SnapshotPreferences/PrecisionPreference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// PrecisionPreference.swift
//
//
// Created by Noah Martin on 9/5/23.
//

import Foundation
import SwiftUI

struct PrecisionPreferenceKey: PreferenceKey {
static func reduce(value: inout Float?, nextValue: () -> Float?) {
value = nextValue()
}

static var defaultValue: Float? = nil
}

extension View {
public func emergeSnapshotPrecision(_ precision: Float?) -> some View {
preference(key: PrecisionPreferenceKey.self, value: precision)
}
}
24 changes: 24 additions & 0 deletions Sources/SnapshotPreferences/RenderingModePreference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// RenderingModePreference.swift
//
//
// Created by Noah Martin on 9/7/23.
//

import Foundation
import SwiftUI
import SnapshotPreviewsCore

struct RenderingModePreferenceKey: PreferenceKey {
static func reduce(value: inout Int?, nextValue: () -> Int?) {
value = nextValue()
}

static var defaultValue: EmergeRenderingMode.RawValue? = nil
}

extension View {
public func emergeRenderingMode(_ renderingMode: EmergeRenderingMode?) -> some View {
preference(key: RenderingModePreferenceKey.self, value: renderingMode?.rawValue)
}
}
24 changes: 19 additions & 5 deletions Sources/SnapshotPreviewsCore/ExpandingViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,28 @@ extension UIView {
}
}

public final class ExpandingViewController<Content: View>: UIHostingController<Content> {
let modifierFinderClass = (NSClassFromString("EmergeModifierFinder") as? NSObject.Type)?.init()
let finder = modifierFinderClass != nil ? Mirror(reflecting: modifierFinderClass!).descendant("finder") as? (any View) -> any View : nil
let modifierState = NSClassFromString("EmergeModifierState") as? NSObject.Type
let stateMirror = modifierState != nil ? Mirror(
reflecting: modifierState!
.perform(NSSelectorFromString("shared"))
.takeUnretainedValue()) : nil

public final class ExpandingViewController<Content: View>: UIHostingController<AnyView> {

private var didCall = false
private var previousHeight: CGFloat?
private let supportsExpansion: Bool

private var heightAnchor: NSLayoutConstraint?

var expansionSettled: (() -> Void)?
var expansionSettled: ((EmergeRenderingMode?, Float?) -> Void)?

init(rootView: Content, layout: PreviewLayout, supportsExpansion: Bool) {
self.supportsExpansion = supportsExpansion
super.init(rootView: rootView)
let newView = finder?(rootView)
super.init(rootView: newView != nil ? AnyView(newView!) : AnyView(rootView))

switch layout {
case let .fixed(width: width, height: height):
Expand All @@ -63,12 +72,14 @@ public final class ExpandingViewController<Content: View>: UIHostingController<C
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func runCallback() {
guard !didCall else { return }

didCall = true
expansionSettled?()
let renderingMode = stateMirror?.descendant("renderingMode") as? EmergeRenderingMode.RawValue
let emergeRenderingMode = renderingMode != nil ? EmergeRenderingMode(rawValue: renderingMode!) : nil
expansionSettled?(emergeRenderingMode, stateMirror?.descendant("precision") as? Float)
}

public override func viewDidLayoutSubviews() {
Expand All @@ -83,6 +94,9 @@ public final class ExpandingViewController<Content: View>: UIHostingController<C
return
}

let expansionPreference = stateMirror?.descendant("expansionPreference") as? Bool

let supportsExpansion = expansionPreference ?? self.supportsExpansion
let scrollView = view.firstScrollView
if let scrollView, supportsExpansion {
let diff = Int(scrollView.contentSize.height - scrollView.visibleContentHeight)
Expand Down
23 changes: 0 additions & 23 deletions Sources/SnapshotPreviewsCore/ExpansionModifier.swift

This file was deleted.

23 changes: 0 additions & 23 deletions Sources/SnapshotPreviewsCore/PrecisionModifier.swift

This file was deleted.

15 changes: 15 additions & 0 deletions Sources/SnapshotPreviewsCore/RenderingMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// EmergeRenderingMode.swift
//
//
// Created by Noah Martin on 10/6/23.
//

import Foundation

public enum EmergeRenderingMode: Int {
// Renders using CALayer render(in:)
case coreAnimation
// Renders using UIView drawHierarchy(in: , afterScreenUpdates: true)
case uiView
}
30 changes: 0 additions & 30 deletions Sources/SnapshotPreviewsCore/RenderingModeModifier.swift

This file was deleted.

3 changes: 1 addition & 2 deletions Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public struct Preview: Identifiable {
displayName = preview.displayName
device = preview.device
layout = preview.layout
let _view = {
_view = {
let children = try extraction.previews.get()

let (v, modifiers) = children[preview.id]
Expand All @@ -23,7 +23,6 @@ public struct Preview: Identifiable {
}
return AnyView(result)
}
self._view = _view
}

#if swift(>=5.9)
Expand Down
14 changes: 7 additions & 7 deletions Sources/SnapshotPreviewsCore/View+Snapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ extension View {
public func snapshot(
layout: PreviewLayout,
window: UIWindow,
supportsExpansion: Bool,
renderingMode: EmergeRenderingMode?,
legacySupportsExpansion: Bool,
legacyRenderingMode: EmergeRenderingMode?,
async: Bool,
completion: @escaping (Result<UIImage, Error>) -> Void)
completion: @escaping (Result<UIImage, Error>, Float?) -> Void)
{
UIView.setAnimationsEnabled(false)
let animationDisabledView = self.transaction { transaction in
transaction.disablesAnimations = true
}
let controller = ExpandingViewController(rootView: animationDisabledView, layout: layout, supportsExpansion: supportsExpansion)
let controller = ExpandingViewController(rootView: animationDisabledView, layout: layout, supportsExpansion: legacySupportsExpansion)
if #available(iOS 16, *) {
controller.sizingOptions = .intrinsicContentSize
}
Expand All @@ -37,14 +37,14 @@ extension View {

let (windowRootVC, containerVC) = Self.setupRootVC(subVC: controller)
window.rootViewController = windowRootVC
controller.expansionSettled = {
controller.expansionSettled = { renderingMode, precision in
if async {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
completion(Self.takeSnapshot(layout: layout, renderingMode: renderingMode, rootVC: containerVC, controller: controller))
completion(Self.takeSnapshot(layout: layout, renderingMode: legacyRenderingMode ?? renderingMode, rootVC: containerVC, controller: controller), precision)
}
} else {
DispatchQueue.main.async {
completion(Self.takeSnapshot(layout: layout, renderingMode: renderingMode, rootVC: containerVC, controller: controller))
completion(Self.takeSnapshot(layout: layout, renderingMode: legacyRenderingMode ?? renderingMode, rootVC: containerVC, controller: controller), precision)
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/SnapshottingSwift/Snapshots.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,21 @@ class Snapshots {

let provider = previewTypes[0]
let preview = provider.previews.filter { $0.previewId == id }[0]
try! display(preview: preview) { imageResult in
try! display(preview: preview) { imageResult, _ in
completion(imageResult, preview)
}
}

@MainActor func display(preview: SnapshotPreviewsCore.Preview, completion: @escaping (Result<UIImage, Error>) -> Void) throws {
@MainActor func display(preview: SnapshotPreviewsCore.Preview, completion: @escaping (Result<UIImage, Error>, Float?) -> Void) throws {
var view = try preview.view()
let supportsExpansion = ViewInspection.shouldExpand(view)
let renderingMode = ViewInspection.renderingMode(of: view)
view = AnyView(PreferredColorSchemeWrapper { view })
view.snapshot(
layout: preview.layout,
window: window,
supportsExpansion: supportsExpansion,
renderingMode: renderingMode,
legacySupportsExpansion: supportsExpansion,
legacyRenderingMode: renderingMode,
async: false,
completion: completion)
}
Expand Down

0 comments on commit 8ff68eb

Please sign in to comment.