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

Adds visionOS support #34

Merged
merged 6 commits into from
Oct 24, 2024
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
33 changes: 27 additions & 6 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ jobs:
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest

- name: Download iOS Simulator
run: |
xcodebuild -downloadPlatform "iOS Simulator"

- name: Build for iOS
run: |
xcodebuild clean build \
-scheme ${{ env.PROJECT_SCHEME }} \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro'
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \
-quiet

build-macos:
runs-on: macos-latest
Expand All @@ -46,7 +51,8 @@ jobs:
xcodebuild clean build \
-scheme ${{ env.PROJECT_SCHEME }} \
-sdk macosx \
-destination 'platform=OS X'
-destination 'platform=OS X' \
-quiet

build-tvos:
runs-on: macos-latest
Expand All @@ -58,13 +64,18 @@ jobs:
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest


- name: Download tvOS Simulator
run: |
xcodebuild -downloadPlatform "tvOS Simulator"

- name: Build for tvOS
run: |
xcodebuild clean build \
-scheme ${{ env.PROJECT_SCHEME }} \
-sdk appletvsimulator \
-destination 'platform=tvOS Simulator,name=Apple TV'
-destination 'platform=tvOS Simulator,name=Apple TV' \
-quiet

build-watchos:
runs-on: macos-latest
Expand All @@ -76,13 +87,18 @@ jobs:
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest

- name: Download watchOS Simulator
run: |
xcodebuild -downloadPlatform "watchOS Simulator"

- name: Build for watchOS
run: |
xcodebuild clean build \
-scheme ${{ env.PROJECT_SCHEME }} \
-sdk watchsimulator \
-destination 'platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm)'
-destination 'platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm)' \
-quiet

build-visionos:
runs-on: macos-latest
Expand All @@ -94,10 +110,15 @@ jobs:
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest

- name: Download visionOS Simulator
run: |
xcodebuild -downloadPlatform "visionOS Simulator"

- name: Build for visionOS
run: |
xcodebuild clean build \
-scheme ${{ env.PROJECT_SCHEME }} \
-sdk xros \
-destination 'platform=visionOS Simulator,name=Apple Vision Pro'
-destination 'platform=visionOS Simulator,name=Apple Vision Pro' \
-quiet
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ let package = Package(
.iOS(.v15),
.tvOS(.v15),
.watchOS(.v8),
.visionOS(.v1),
],
products: [
.library(name: "MarkdownView", targets: ["MarkdownView"]),
Expand Down
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ You can use MarkdownView in the following platforms:
* iOS 15.0+
* watchOS 8.0+
* tvOS 15.0+
* visionOS 1.0+

# Highlighted Features

Expand Down Expand Up @@ -114,13 +115,6 @@ MarkdownView(text: markdownText)

The implementation of the block directive is exactly the same way.

# Todos

- [x] watchOS support. (specifically watchOS 8.0+)
- [x] Table support for iOS 15.0, macOS 12.0 and tvOS 15.0.
- [x] Add support for font size adjustments using SwiftUI built-in `.font(_:)` modifier.
- [x] Built-in image providers improvements.

# Swift Package Manager

In your `Package.swift` Swift Package Manager manifest, add the following dependency to your `dependencies` argument:
Expand Down
184 changes: 184 additions & 0 deletions Sources/MarkdownView/Helper/SwiftUI+OnChangeModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import SwiftUI

extension View {

/// Adds a modifier for this view that fires an action when a specific
/// value changes.
///
/// You can use `onChange` to trigger a side effect as the result of a
/// value changing, such as an `Environment` key or a `Binding`.
///
/// The system may call the action closure on the main actor, so avoid
/// long-running tasks in the closure. If you need to perform such tasks,
/// detach an asynchronous background task.
///
/// When the value changes, the new version of the closure will be called,
/// so any captured values will have their values from the time that the
/// observed value has its new value. The old and new observed values are
/// passed into the closure. In the following code example, `PlayerView`
/// passes both the old and new values to the model.
///
/// struct PlayerView: View {
/// var episode: Episode
/// @State private var playState: PlayState = .paused
///
/// var body: some View {
/// VStack {
/// Text(episode.title)
/// Text(episode.showTitle)
/// PlayButton(playState: $playState)
/// }
/// .onChange(of: playState) { oldState, newState in
/// model.playStateDidChange(from: oldState, to: newState)
/// }
/// }
/// }
///
/// - Parameters:
/// - value: The value to check against when determining whether
/// to run the closure.
/// - initial: Whether the action should be run when this view initially
/// appears.
/// - action: A closure to run when the value changes.
/// - oldValue: The old value that failed the comparison check (or the
/// initial value when requested).
/// - newValue: The new value that failed the comparison check.
///
/// - Returns: A view that fires an action when the specified value changes.
@_disfavoredOverload
@available(iOS, introduced: 14.0, deprecated: 17.0)
@available(macOS, introduced: 11.0, deprecated: 14.0)
@available(tvOS, introduced: 14.0, deprecated: 17.0)
@available(watchOS, introduced: 7.0, deprecated: 10.0)
@available(visionOS, unavailable)
public func onChange<E: Equatable>(
of value: E,
initial: Bool = false,
_ action: @escaping (_ oldValue: E, _ newValue: E) -> Void
) -> some View {
modifier(
OnChangeModifier(
value: value,
initial: initial,
action: .init(action)
)
)
}

/// Adds a modifier for this view that fires an action when a specific
/// value changes.
///
/// You can use `onChange` to trigger a side effect as the result of a
/// value changing, such as an `Environment` key or a `Binding`.
///
/// The system may call the action closure on the main actor, so avoid
/// long-running tasks in the closure. If you need to perform such tasks,
/// detach an asynchronous background task.
///
/// When the value changes, the new version of the closure will be called,
/// so any captured values will have their values from the time that the
/// observed value has its new value. In the following code example,
/// `PlayerView` calls into its model when `playState` changes model.
///
/// struct PlayerView: View {
/// var episode: Episode
/// @State private var playState: PlayState = .paused
///
/// var body: some View {
/// VStack {
/// Text(episode.title)
/// Text(episode.showTitle)
/// PlayButton(playState: $playState)
/// }
/// .onChange(of: playState) {
/// model.playStateDidChange(state: playState)
/// }
/// }
/// }
///
/// - Parameters:
/// - value: The value to check against when determining whether
/// to run the closure.
/// - initial: Whether the action should be run when this view initially
/// appears.
/// - action: A closure to run when the value changes.
///
/// - Returns: A view that fires an action when the specified value changes.
@_disfavoredOverload
@available(iOS, introduced: 14.0, deprecated: 17.0)
@available(macOS, introduced: 11.0, deprecated: 14.0)
@available(tvOS, introduced: 14.0, deprecated: 17.0)
@available(watchOS, introduced: 7.0, deprecated: 10.0)
@available(visionOS, unavailable)
public func onChange<E: Equatable>(
of value: E,
initial: Bool = false,
_ action: @escaping () -> Void
) -> some View {
modifier(
OnChangeModifier(
value: value,
initial: initial,
action: .init(action)
)
)
}
}

@available(iOS, introduced: 14.0, deprecated: 17.0)
@available(macOS, introduced: 11.0, deprecated: 14.0)
@available(tvOS, introduced: 14.0, deprecated: 17.0)
@available(watchOS, introduced: 7.0, deprecated: 10.0)
@available(visionOS, unavailable)
struct OnChangeModifier<E: Equatable>: ViewModifier {
var value: E
var initial: Bool

struct ActionPack {
var actionWithValues: ((E, E) -> Void)?
var simpleAction: (() -> Void)?

func callAsFunction(before: E, after: E) {
if let actionWithValues {
actionWithValues(before, after)
} else if let simpleAction {
simpleAction()
}
}

init(_ action: @escaping (E, E) -> Void) {
self.actionWithValues = action
self.simpleAction = nil
}

init(_ action: @escaping () -> Void) {
self.simpleAction = action
self.actionWithValues = nil
}
}
var action: ActionPack

func body(content: Content) -> some View {
if #available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) {
content
.onChange(
of: value,
initial: initial,
action.callAsFunction(before:after:)
)
} else {
content
.onAppear(perform: initialAction)
.onChange(of: value) { [value] newValue in
Task { @MainActor in
action(before: value, after: newValue)
}
}
}
}

private func initialAction() {
guard initial else { return }
action(before: value, after: value)
}
}
4 changes: 2 additions & 2 deletions Sources/MarkdownView/MarkdownView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ public struct MarkdownView: View {
// Received a debouncedText, we need to reload MarkdownView.
.onReceive(contentUpdater.textUpdater, perform: updateView(text:))
// Push current text, waiting for next update.
.onChange(of: text, perform: contentUpdater.push(_:))
.onChange(of: text) { contentUpdater.push(text) }
}
.if(renderingMode == .immediate && renderingThread == .background) { content in
content
// Immediately update MarkdownView when text changes.
.onChange(of: text, perform: updateView(text:))
.onChange(of: text) { updateView(text: text) }
}
// Load view immediately after the first launch.
// Receive configuration changes and reload MarkdownView to fit.
Expand Down
6 changes: 3 additions & 3 deletions Sources/MarkdownView/Renderer/Renderer+CodeBlock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ extension Renderer {
extension Renderer {
mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> Result {
Result {
#if os(watchOS) || os(tvOS)
SwiftUI.Text(codeBlock.code)
#else
#if canImport(Highlightr)
HighlightedCodeBlock(
language: codeBlock.language,
code: codeBlock.code,
theme: configuration.codeBlockTheme
)
#else
SwiftUI.Text(codeBlock.code)
#endif
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/MarkdownView/View/SVGView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ fileprivate struct _SVGViewBridge: NSViewRepresentable {
Coordinator(updateSize: updateSize)
}
}
#elseif os(iOS)
#elseif os(iOS) || os(visionOS)
fileprivate struct _SVGViewBridge: UIViewRepresentable {
var html: String
var updateSize: (CGSize) -> Void
Expand Down
Loading