I am trying to assign a custom label style based on my icon alignment value, that is left or right. if it is left, the icon comes before text, if it is right the icon comes after it. These are my custom label styles:
var iconAlignment: IconAlignment
enum IconAlignment {
case left
case right
}
struct IconFirstLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon
configuration.title
}
}
}
struct IconFirstLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.title
configuration.icon
}
}
}
I want to do this now:
Label { Text("hi") } icon : {Image(systemName: "plus")}
.labelStyle(iconAlignment == .left ? IconFirstLabelStyle() : TextFirstLabelStyle())
but this causes an issue and Xcode tells me these are two different structs, even though they are inheriting the same protocol. Is there any way to use ternary here, or should I go a long approach with if else?
Ternary operator requires both parts to be of the same type.
A simple solution for this is just to use one style type and inject condition via argument, like
Label { Text("hi") } icon : {Image(systemName: "plus")}
.labelStyle(IconAlignedLabelStyle(alignIcon: iconAlignment))
struct IconAlignedLabelStyle: LabelStyle {
var alignIcon = IconAlignment.left // << default one !!
func makeBody(configuration: Configuration) -> some View {
HStack {
if let alignIcon == .left {
configuration.icon
configuration.title
} else {
configuration.title
configuration.icon
}
}
}
}
You say "even though they are inheriting the same protocol"—that does not display the understanding that some, and the ternary operator, each require one type. Such as this:
labelStyle(
iconAlignment == .left
? AnyStyle(IconFirstLabelStyle())
: AnyStyle(TextFirstLabelStyle())
)
import SwiftUI
/// A type-erased "`Style`".
public struct AnyStyle<Configuration> {
private let makeBody: (Configuration) -> AnyView
}
// MARK: - public
public extension AnyStyle {
init(makeBody: #escaping (Configuration) -> some View) {
self.makeBody = { .init(makeBody($0)) }
}
func makeBody(configuration: Configuration) -> some View {
makeBody(configuration)
}
}
// MARK: - LabelStyle
extension AnyStyle: LabelStyle where Configuration == LabelStyleConfiguration {
public init(_ style: some LabelStyle) {
self.init(makeBody: style.makeBody)
}
}
// MARK: - ButtonStyle
extension AnyStyle: ButtonStyle where Configuration == ButtonStyleConfiguration {
public init(_ style: some ButtonStyle) {
self.init(makeBody: style.makeBody)
}
}
// etc.
Related
I want to implement a modifier setting for Texts in a similar way as it already exists for Buttons.
Aka:
Button( ... )
.buttonStyle(.plain) // <-- .plain and not PlainStyle()
Problem
Of course I cannot use an opaque which is not really the same. If it would be a View I could wrap it in an AnyView but for ViewModifiers I need another solution.
Error: Function declares an opaque return type,but the return statements in its body do not have matching underlying types
Maybe it is a bonus idea to have something like a .textStyle(.title) modifier but in my eyes, it could reduce my code to write enormously.
Source
struct TitleStyle: ViewModifier {
func body(content: Content) -> some View {
...
}
}
struct BodyStyle: ViewModifier {
func body(content: Content) -> some View {
...
}
}
enum TextStyle {
case title
case body
// Error: Function declares an opaque return type,
// but the return statements in its body do not have matching underlying types
var modifier: some ViewModifier {
switch self
{
case .title: return TitleStyle()
case .body: return BodyStyle()
}
}
}
It works different way. As all this is around generics we need to restrict declarations for known concrete types.
So, having TitleStyle and BodyStyle declared and concrete, we can specify
extension ViewModifier where Self == TitleStyle {
static var title: TitleStyle { TitleStyle() }
}
extension ViewModifier where Self == BodyStyle {
static var body: BodyStyle { BodyStyle() }
}
and then declare extension to use above like
extension View {
func textStyle<Style: ViewModifier>(_ style: Style) -> some View {
ModifiedContent(content: self, modifier: style)
}
}
so as a result we can do as demo
struct Demo_Previews: PreviewProvider {
static var previews: some View {
Text("Demo")
.textStyle(.title)
}
}
Prepared with Xcode 13.4 / iOS 15.5
Test module in GitHub
I have this:
VStack {
List {
LazyVGrid(columns: gridItemLayout) {
ForEach(viewModel.objects, id: \.fileGroupUUID) { item in
AlbumItemsScreenCell(object: item, viewModel: viewModel, config: Self.config)
.onTapGesture {
switch viewModel.changeMode {
case .moving, .sharing, .moveAll:
viewModel.toggleItemToChange(item: item)
case .none:
object = item
viewModel.showCellDetails = true
}
}
.onLongPressGesture {
viewModel.restartDownload(fileGroupUUID: item.fileGroupUUID)
}
} // end ForEach
} // end LazyVGrid
}
.listStyle(PlainListStyle())
.refreshable {
viewModel.refresh()
}
.padding(5)
// Mostly this is to animate updates from the menu. E.g., the sorting order.
.animation(.easeInOut)
// Had a problem with return animation for a while: https://stackoverflow.com/questions/65101561
// The solution was to take the NavigationLink out of the scrollview/LazyVGrid above.
if let object = object {
// The `NavigationLink` works here because the `MenuNavBar` contains a `NavigationView`.
NavigationLink(
destination:
ObjectDetailsView(object: object, model: ObjectDetailsModel(object: object)),
isActive:
$viewModel.showCellDetails) {
EmptyView()
}
.frame(width: 0, height: 0)
.disabled(true)
} // end if
} // end VStack
AlbumItemsScreenCell:
struct AlbumItemsScreenCell: View {
#StateObject var object:ServerObjectModel
#StateObject var viewModel:AlbumItemsViewModel
let config: IconConfig
#Environment(\.colorScheme) var colorScheme
var body: some View {
AnyIcon(model: AnyIconModel(object: object), config: config,
emptyUpperRightView: viewModel.changeMode == .none,
upperRightView: {
UpperRightChangeIcon(object: object, viewModel: viewModel)
})
}
}
When a user taps one of the cells, this causes navigation to a details screen. Sometimes when the user returns from that navigation, the cell in the upper left disappears:
https://www.dropbox.com/s/mi6j2ie7h8dcdm0/disappearingCell.mp4?dl=0
My current hypothesis about the issue is that when user actions in that details screen take actions which change viewModel.objects, this causes the disappearing cell problem. I'll be testing this hypothesis shortly.
----- Update, 11/1/21 ------
Well, that hypothesis was wrong. I now understand the structure of the problem more clearly. Still don't have a fix though.
Tapping on one of the AlbumItemsScreenCells navigates to a details screen (I've added to the code above to show that). In the details screen user actions can cause a comment count to get reset, which sends a Notification.
A model in the AlbumItemsScreenCell listens for these notification (for the specific cell) and resets a badge on the cell.
Here is that model:
class AnyIconModel: ObservableObject, CommentCountsObserverDelegate, MediaItemBadgeObserverDelegate, NewItemBadgeObserverDelegate {
#Published var mediaItemBadge: MediaItemBadge?
#Published var unreadCountBadgeText: String?
#Published var newItem: Bool = false
var mediaItemCommentCount:CommentCountsObserver!
let object: ServerObjectModel
var mediaItemBadgeObserver: MediaItemBadgeObserver!
var newItemObserver: NewItemBadgeObserver!
init(object: ServerObjectModel) {
self.object = object
// This is causing https://stackoverflow.com/questions/69783232/cell-in-list-with-lazyvgrid-disappears-sometimes
mediaItemCommentCount = CommentCountsObserver(object: object, delegate: self)
mediaItemBadgeObserver = MediaItemBadgeObserver(object: object, delegate: self)
newItemObserver = NewItemBadgeObserver(object: object, delegate: self)
}
}
The unreadCountBadgeText gets changed (on the main thread) by the observer when the Notification is received.
So, in summary, the badge on the cell gets changed while the screen with the cells is not displayed-- the details screen is displayed.
I had been using the following conditional modifier:
extension View {
public func enabled(_ enabled: Bool) -> some View {
return self.disabled(!enabled)
}
// https://forums.swift.org/t/conditionally-apply-modifier-in-swiftui/32815/16
#ViewBuilder func `if`<T>(_ condition: Bool, transform: (Self) -> T) -> some View where T : View {
if condition {
transform(self)
} else {
self
}
}
}
to display the badge on the AlbumItemsScreenCell.
The original badge looked like this:
extension View {
func upperLeftBadge(_ badgeText: String) -> some View {
return self.modifier(UpperLeftBadge(badgeText))
}
}
struct UpperLeftBadge: ViewModifier {
let badgeText: String
init(_ badgeText: String) {
self.badgeText = badgeText
}
func body(content: Content) -> some View {
content
.overlay(
ZStack {
Badge(badgeText)
}
.padding([.top, .leading], 5),
alignment: .topLeading
)
}
}
i.e., the usage looked like this in the cell:
.if(condition) {
$0.upperLeftBadge(badgeText)
}
when I changed the modifier to use it with out this .if modifier, and used it directly, the issue went away:
extension View {
func upperLeftBadge(_ badgeText: String?) -> some View {
return self.modifier(UpperLeftBadge(badgeText: badgeText))
}
}
struct UpperLeftBadge: ViewModifier {
let badgeText: String?
func body(content: Content) -> some View {
content
.overlay(
ZStack {
if let badgeText = badgeText {
Badge(badgeText)
}
}
.padding([.top, .leading], 5),
alignment: .topLeading
)
}
}
It may sounds weird, but in my tvOS app i disable the animations
and cells(Views) stops to disappear
So try to remove .animation(.easeInOut)
I'm using this .if extension which works great and should be added to SwiftUI, however it won't work in this case to check #available because #available may only be used as condition of an 'if', 'guard' or 'while' statement How can I make it work with this .if, anyway?
//Does not compile for #available may only be used as condition of an 'if', 'guard' or 'while' statement
ForEach...
.if(#available(iOS 15.0, *)) { $0.refreshable {
} }
extension View {
#ViewBuilder
func `if`<Transform: View>(
_ condition: Bool,
transform: (Self) -> Transform
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
I would typically just do .if(true) and then check availability inside the closure. Unfortunately, we can't check for availability in the if modifier.
Example:
struct ContentView: View {
var body: some View {
List {
Text("Hello")
/* More list items... */
}
.if(true) {
if #available(iOS 15, *) {
$0.refreshable {
//
}
} else {
$0
}
}
}
}
Added #ViewBuilder to transform parameter so you can return different View types in the if:
extension View {
#ViewBuilder func `if`<Transform: View>(_ condition: Bool, #ViewBuilder transform: (Self) -> Transform) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
I want to respond to key presses, such as the esc key on macOS/OSX, and when using an external keyboard on iPad. How can I do this?
I have thought of using #available/#available with SwiftUI's onExitCommand, which looked very promising, but unfortunately that is only for macOS/OSX. How can I respond to key presses in SwiftUI for more than just macOS/OSX?
Update: SwiftUI 2 now has .keyboardShortcut(_:modifiers:).
OLD ANSWER:
With thanks to #Asperi to pointing me in the right direction, I have now managed to get this working.
The solution was to use UIKeyCommand. Here is what I did, but you can adapt it differently depending on your situation.
I have an #EnvironmentObject called AppState, which helps me set the delegate, so they keyboard input can be different depending on the view currently showing.
protocol KeyInput {
func onKeyPress(_ key: String)
}
class KeyInputController<Content: View>: UIHostingController<Content> {
private let state: AppState
init(rootView: Content, state: AppState) {
self.state = state
super.init(rootView: rootView)
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func becomeFirstResponder() -> Bool {
true
}
override var keyCommands: [UIKeyCommand]? {
switch state.current {
case .usingApp:
return [
UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(keyPressed(_:)))
]
default:
return nil
}
}
#objc private func keyPressed(_ sender: UIKeyCommand) {
guard let key = sender.input else { return }
state.delegate?.onKeyPress(key)
}
}
AppState (#EnvironmentObject):
class AppState: ObservableObject {
var delegate: KeyInput?
/* ... */
}
And the scene delegate looks something like:
let stateObject = AppState()
let contentView = ContentView()
.environmentObject(stateObject)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = KeyInputController(rootView: contentView, state: stateObject)
/* ... */
}
This makes it really easy to now add functionality depending on the keys pressed.
Conform to KeyInput, e.g.:
struct ContentView: View, KeyInput {
/* ... */
var body: some View {
Text("Hello world!")
.onAppear {
self.state.delegate = self
}
}
func onKeyPress(_ key: String) {
print(key)
guard key == UIKeyCommand.inputEscape else { return }
// esc key was pressed
/* ... */
}
}
On macOS and tvOS there is an onExitCommand(perform:) modifier for views.
From Apple's documentation:
Sets up an action that triggers in response to receiving the exit command while the view has focus.
The user generates an exit command by pressing the Menu button on tvOS, or the escape key on macOS.
For example:
struct ContentView: View {
var body: some View {
VStack {
TextField("Top", text: .constant(""))
.onExitCommand(perform: {
print("Exit from top text field")
})
TextField("Bottom", text: .constant(""))
.onExitCommand(perform: {
print("Exit from bottom text field")
})
}
.padding()
}
}
An easier solution is the .onExitCommand modifier.
As #Chuck H commented, this does not work if the text field is inside a ScrollView or like in my case, a Section.
But I discovered that just by embedding the TextField and its .onExitCommand inside a HStack or VStack, the .onExitCommand just works.
HStack { // This HStack is for .onExitCommand to work
TextField("Nombre del proyecto", text: $texto)
.onExitCommand(perform: {
print("Cancelando edición....")
})
.textFieldStyle(PlainTextFieldStyle())
.lineLimit(1)
}
.onAppear() {
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (aEvent) -> NSEvent? in
if aEvent.keyCode == 53 { // if esc pressed
appDelegate.hideMainWnd()
return nil // do not do "beep" sound
}
return aEvent
}
}
I'm trying to get my navigation view style to be stacked on iPad but default on iPhone.
Code:
.navigationViewStyle(UIDevice.current.userInterfaceIdiom == .pad ? StackNavigationViewStyle() : DefaultNavigationViewStyle())
Giving me the error:
Result values in '? :' expression have mismatching types 'StackNavigationViewStyle' and 'DefaultNavigationViewStyle'
Are these not both NavigationViewStyle subclasses?
I recommend to extract it into simple wrapper modifier and use it in place where needed. Here is modifier:
Update:
extension View {
#ViewBuilder
public func currentDeviceNavigationViewStyle() -> some View {
if UIDevice.current.userInterfaceIdiom == .pad {
self.navigationViewStyle(StackNavigationViewStyle())
} else {
self.navigationViewStyle(DefaultNavigationViewStyle())
}
}
}
SwiftUI 1.0 (backward-compatible)
extension View {
public func currentDeviceNavigationViewStyle() -> AnyView {
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
} else {
return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
}
}
}
Thats because ? : should always return values with same type.
You can implement a custom conditional modifier extension like this:
extension View {
public func modify<T, U>(if condition: Bool, then modifierT: T, else modifierU: U) -> some View where T: ViewModifier, U: ViewModifier {
Group {
if condition {
modifier(modifierT)
} else {
modifier(modifierU)
}
}
}
}
Implement custom modifiers like this:
struct IPadNavigationViewStyle: ViewModifier {
func body(content: Content) -> some View { content.navigationViewStyle(StackNavigationViewStyle()) }
}
struct IPhoneNavigationViewStyle: ViewModifier {
func body(content: Content) -> some View { content.navigationViewStyle(DefaultNavigationViewStyle()) }
}
and then use it like:
.modify(if: UIDevice.current.userInterfaceIdiom == .pad, then: IPadNavigationViewStyle(), else: IPhoneNavigationViewStyle() )