Skip to content

Commit

Permalink
Fix Text constructors. Support .task modifier
Browse files Browse the repository at this point in the history
  • Loading branch information
aabewhite committed Sep 24, 2023
1 parent f0ab72e commit a15fec2
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 213 deletions.
30 changes: 15 additions & 15 deletions Sources/SkipUI/SkipUI/Containers/Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,33 +169,33 @@ public struct NavigationStack<Root> : View where Root: View {
// compose the root view with a custom composer that will capture the destinations. Note that 'root' is just a reference to
// the enclosing ComposeView, so a custom composer is the only way to receive a reference to our actual root view.
// Have to use rememberSaveable for e.g. a nav stack in each tab
// SKIP INSERT: var destinations by rememberSaveable(stateSaver = context.stateSaver as Saver<NavigationDestinations?, Any>) { mutableStateOf<NavigationDestinations?>(null) }
if destinations == nil {
let destinations = rememberSaveable(stateSaver: context.stateSaver as! Saver<NavigationDestinations?, Any>) { mutableStateOf<NavigationDestinations?>(nil) }
if destinations.value == nil {
root.Compose(context: context.content(composer: { view, context in
destinations = (view as? NavigationDestinationView)?.destinations ?? [:]
destinations.value = (view as? NavigationDestinationView)?.destinations ?? [:]
}))
}

let navController = rememberNavController()
// SKIP INSERT: val navigator by rememberSaveable(stateSaver = context.stateSaver as Saver<Navigator, Any>) { mutableStateOf(Navigator(navController = navController, destinations = destinations ?: dictionaryOf())) }
navigator.navController = navController // May change on recompose
navigator.syncState()
let navigator = rememberSaveable(stateSaver: context.stateSaver as! Saver<Navigator, Any>) { mutableStateOf(Navigator(navController: navController, destinations: destinations.value ?? dictionaryOf())) }
navigator.value.navController = navController // May change on recompose
navigator.value.syncState()

// SKIP INSERT: val providedNavigator = LocalNavigator provides navigator
// SKIP INSERT: val providedNavigator = LocalNavigator provides navigator.value
CompositionLocalProvider(providedNavigator) {
NavHost(navController: navController, startDestination: Navigator.rootRoute, modifier: context.modifier) {
composable(route: Navigator.rootRoute) { entry in
if let state = navigator.state(for: entry) {
if let state = navigator.value.state(for: entry) {
let entryContext = context.content(stateSaver: state.stateSaver)
ComposeEntry(navController: navController, isRoot: true, context: entryContext) { context in
root.Compose(context: context)
}
}
}
if let destinations {
if let destinations = destinations.value {
for (targetType, viewBuilder) in destinations {
composable(route: Navigator.route(for: targetType, valueString: "{identifier}"), arguments: listOf(navArgument("identifier") { type = NavType.StringType })) { entry in
if let state = navigator.state(for: entry), let targetValue = state.targetValue {
if let state = navigator.value.state(for: entry), let targetValue = state.targetValue {
let entryContext = context.content(stateSaver: state.stateSaver)
ComposeEntry(navController: navController, isRoot: false, context: entryContext) { context in
viewBuilder(targetValue).Compose(context: context)
Expand All @@ -210,10 +210,10 @@ public struct NavigationStack<Root> : View where Root: View {

// SKIP INSERT: @OptIn(ExperimentalMaterial3Api::class)
@Composable private func ComposeEntry(navController: NavHostController, isRoot: Bool, context: ComposeContext, content: @Composable (ComposeContext) -> Void) {
// SKIP INSERT: var preferenceUpdates by remember { mutableStateOf(0) }
let _ = preferenceUpdates // Read so that it can trigger recompose on change
let preferenceUpdates = remember { mutableStateOf(0) }
let _ = preferenceUpdates.value // Read so that it can trigger recompose on change

// SKIP INSERT: var title by rememberSaveable(stateSaver = context.stateSaver as Saver<String, Any>) { mutableStateOf(NavigationTitlePreferenceKey.defaultValue) }
let title = rememberSaveable(stateSaver: context.stateSaver as! Saver<String, Any>) { mutableStateOf(NavigationTitlePreferenceKey.defaultValue) }

// We place the top bar scaffold within each entry rather than at the navigation controller level. There isn't a fluid animation
// between navigation bar states on Android, and it is simpler to only hoist navigation bar preferences to this level
Expand All @@ -227,7 +227,7 @@ public struct NavigationStack<Root> : View where Root: View {
titleContentColor: MaterialTheme.colorScheme.onSurface
),
title: {
androidx.compose.material3.Text(title, maxLines: 1, overflow: TextOverflow.Ellipsis)
androidx.compose.material3.Text(title.value, maxLines: 1, overflow: TextOverflow.Ellipsis)
},
navigationIcon: {
if !isRoot {
Expand All @@ -240,7 +240,7 @@ public struct NavigationStack<Root> : View where Root: View {
)
}
) { padding in
let titlePreference = Preference<String>(key: NavigationTitlePreferenceKey.self, update: { title = $0 }, didChange: { preferenceUpdates += 1 })
let titlePreference = Preference<String>(key: NavigationTitlePreferenceKey.self, update: { title.value = $0 }, didChange: { preferenceUpdates.value += 1 })
PreferenceValues.shared.collectPreferences([titlePreference]) {
Box(modifier: Modifier.padding(padding).fillMaxSize().then(context.modifier), contentAlignment: androidx.compose.ui.Alignment.Center) {
content(context.content())
Expand Down
6 changes: 3 additions & 3 deletions Sources/SkipUI/SkipUI/Environment/PreferenceKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ extension View {
public func preference(key: Any.Type, value: Any) -> some View {
#if SKIP
return ComposeModifierView(contentView: self) { view, context in
// SKIP INSERT: var pvalue by remember { mutableStateOf<Any?>(null) }
PreferenceValues.shared.preference(key: key)?.reduce(savedValue: pvalue, newValue: value)
pvalue = value
let pvalue = remember { mutableStateOf<Any?>(nil) }
PreferenceValues.shared.preference(key: key)?.reduce(savedValue: pvalue.value, newValue: value)
pvalue.value = value
view.Compose(context: context)
}
#else
Expand Down
127 changes: 0 additions & 127 deletions Sources/SkipUI/SkipUI/Environment/ViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9853,133 +9853,6 @@ extension View {

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension View {

/// Adds an asynchronous task to perform before this view appears.
///
/// Use this modifier to perform an asynchronous task with a lifetime that
/// matches that of the modified view. If the task doesn't finish
/// before SkipUI removes the view or the view changes identity, SkipUI
/// cancels the task.
///
/// Use the `await` keyword inside the task to
/// wait for an asynchronous call to complete, or to wait on the values of
/// an
/// instance. For example, you can modify a ``Text`` view to start a task
/// that loads content from a remote resource:
///
/// let url = URL(string: "https://example.com")!
/// @State private var message = "Loading..."
///
/// var body: some View {
/// Text(message)
/// .task {
/// do {
/// var receivedLines = [String]()
/// for try await line in url.lines {
/// receivedLines.append(line)
/// message = "Received \(receivedLines.count) lines"
/// }
/// } catch {
/// message = "Failed to load"
/// }
/// }
/// }
///
/// This example uses the
/// method to get the content stored at the specified
/// as an
/// asynchronous sequence of strings. When each new line arrives, the body
/// of the `for`-`await`-`in` loop stores the line in an array of strings
/// and updates the content of the text view to report the latest line
/// count.
///
/// - Parameters:
/// - priority: The task priority to use when creating the asynchronous
/// task. The default priority is
/// .
/// - action: A closure that SkipUI calls as an asynchronous task
/// before the view appears. SkipUI will automatically cancel the task
/// at some point after the view disappears before the action completes.
///
///
/// - Returns: A view that runs the specified action asynchronously before
/// the view appears.
public func task(priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable () async -> Void) -> some View { return stubView() }


/// Adds a task to perform before this view appears or when a specified
/// value changes.
///
/// This method behaves like ``View/task(priority:_:)``, except that it also
/// cancels and recreates the task when a specified value changes. To detect
/// a change, the modifier tests whether a new value for the `id` parameter
/// equals the previous value. For this to work,
/// the value's type must conform to the
/// protocol.
///
/// For example, if you define an equatable `Server` type that posts custom
/// notifications whenever its state changes --- for example, from _signed
/// out_ to _signed in_ --- you can use the task modifier to update
/// the contents of a ``Text`` view to reflect the state of the
/// currently selected server:
///
/// Text(status ?? "Signed Out")
/// .task(id: server) {
/// let sequence = NotificationCenter.default.notifications(
/// named: .didChangeStatus,
/// object: server)
/// for try await notification in sequence {
/// status = notification.userInfo["status"] as? String
/// }
/// }
///
/// This example uses the
/// method to wait indefinitely for an asynchronous sequence of
/// notifications, given by an
/// instance.
///
/// Elsewhere, the server defines a custom `didUpdateStatus` notification:
///
/// extension NSNotification.Name {
/// static var didUpdateStatus: NSNotification.Name {
/// NSNotification.Name("didUpdateStatus")
/// }
/// }
///
/// The server then posts a notification of this type whenever its status
/// changes, like after the user signs in:
///
/// let notification = Notification(
/// name: .didUpdateStatus,
/// object: self,
/// userInfo: ["status": "Signed In"])
/// NotificationCenter.default.post(notification)
///
/// The task attached to the ``Text`` view gets and displays the status
/// value from the notification's user information dictionary. When the user
/// chooses a different server, SkipUI cancels the task and creates a new
/// one, which then starts waiting for notifications from the new server.
///
/// - Parameters:
/// - id: The value to observe for changes. The value must conform
/// to the
/// protocol.
/// - priority: The task priority to use when creating the asynchronous
/// task. The default priority is
/// .
/// - action: A closure that SkipUI calls as an asynchronous task
/// before the view appears. SkipUI can automatically cancel the task
/// after the view disappears before the action completes. If the
/// `id` value changes, SkipUI cancels and restarts the task.
///
/// - Returns: A view that runs the specified action asynchronously before
/// the view appears, or restarts the task with the `id` value changes.
public func task<T>(id value: T, priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable () async -> Void) -> some View where T : Equatable { return stubView() }

}

@available(iOS 16.0, macOS 13.0, watchOS 7.0, *)
@available(tvOS, unavailable)
extension View {
Expand Down
65 changes: 4 additions & 61 deletions Sources/SkipUI/SkipUI/Text/LocalizedStringKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,81 +14,24 @@ import struct Foundation.DateComponents
import struct Foundation.DateInterval
import struct Foundation.AttributedString
import struct Foundation.LocalizedStringResource
#else
#endif

/// The key used to look up an entry in a strings file or strings dictionary
/// file.
///
/// Initializers for several SkipUI types -- such as ``Text``, ``Toggle``,
/// ``Picker`` and others -- implicitly look up a localized string when you
/// provide a string literal. When you use the initializer `Text("Hello")`,
/// SkipUI creates a `LocalizedStringKey` for you and uses that to look up a
/// localization of the `Hello` string. This works because `LocalizedStringKey`
/// conforms to
/// .
///
/// Types whose initializers take a `LocalizedStringKey` usually have
/// a corresponding initializer that accepts a parameter that conforms to
/// . Passing
/// a `String` variable to these initializers avoids localization, which is
/// usually appropriate when the variable contains a user-provided value.
///
/// As a general rule, use a string literal argument when you want
/// localization, and a string variable argument when you don't. In the case
/// where you want to localize the value of a string variable, use the string to
/// create a new `LocalizedStringKey` instance.
///
/// The following example shows how to create ``Text`` instances both
/// with and without localization. The title parameter provided to the
/// ``Section`` is a literal string, so SkipUI creates a
/// `LocalizedStringKey` for it. However, the string entries in the
/// `messageStore.today` array are `String` variables, so the ``Text`` views
/// in the list use the string values verbatim.
///
/// List {
/// Section(header: Text("Today")) {
/// ForEach(messageStore.today) { message in
/// Text(message.title)
/// }
/// }
/// }
///
/// If the app is localized into Japanese with the following
/// translation of its `Localizable.strings` file:
///
/// ```other
/// "Today" = "今日";
/// ```
///
/// When run in Japanese, the example produces a
/// list like the following, localizing "Today" for the section header, but not
/// the list items.
///
/// ![A list with a single section header displayed in Japanese.
/// The items in the list are all in English: New for Monday, Account update,
/// and Server
/// maintenance.](SkipUI-LocalizedStringKey-Today-List-Japanese.png)
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct LocalizedStringKey : Equatable {
public struct LocalizedStringKey : Equatable {
public var value: String

/// Creates a localized string key from the given string value.
///
/// - Parameter value: The string to use as a localization key.
public init(_ value: String) {
self.value = value
}

/// Creates a localized string key from the given string literal.
///
/// - Parameter value: The string literal to use as a localization key.
public init(stringLiteral value: String) {
self.value = value
}
}

#if !SKIP

// TODO: Process for use in SkipUI

extension LocalizedStringKey : ExpressibleByStringInterpolation {

/// Creates a localized string key from the given string interpolation.
Expand Down
6 changes: 5 additions & 1 deletion Sources/SkipUI/SkipUI/Text/Text.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ public struct Text: View, Equatable, Sendable {
self.text = verbatim
}

public init(_ text: String, bundle: Bundle? = nil, comment: StaticString? = nil) {
public init(_ text: String) {
self.text = text
}

public init(_ key: LocalizedStringKey, bundle: Bundle? = nil, comment: StaticString? = nil) {
self.text = key.value
}

public init(_ resource: LocalizedStringResource) {
self.text = resource.key
Expand Down
26 changes: 23 additions & 3 deletions Sources/SkipUI/SkipUI/View/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
Expand Down Expand Up @@ -186,7 +188,7 @@ extension View {
$0.modifier = $0.modifier.rotate(Float(angle.degrees))
}
#else
return stubView()
return self
#endif
}

Expand All @@ -195,7 +197,7 @@ extension View {
#if SKIP
fatalError()
#else
return stubView()
return self
#endif
}

Expand Down Expand Up @@ -223,14 +225,32 @@ extension View {
$0.modifier = $0.modifier.scale(scaleX: Float(x), scaleY: Float(y))
}
#else
return stubView()
return self
#endif
}

@available(*, unavailable)
public func scaleEffect(x: CGFloat = 1.0, y: CGFloat = 1.0, anchor: UnitPoint) -> some View {
return scaleEffect(x: x, y: y)
}

public func task(priority: TaskPriority = .userInitiated, _ action: @escaping () async -> Void) -> some View {
return task(id: 0, priority: priority, action)
}

public func task(id value: Any, priority: TaskPriority = .userInitiated, _ action: @escaping () async -> Void) -> some View {
#if SKIP
return ComposeModifierView(contentView: self) { view, context in
let handler = rememberUpdatedState(action)
LaunchedEffect(value) {
handler.value()
}
view.Compose(context: context)
}
#else
return self
#endif
}
}

#if !SKIP
Expand Down
Loading

0 comments on commit a15fec2

Please sign in to comment.