How to apply modifier or view by condition - swiftui

#State var modifierEnabled : Bool
struct BlankModifier: ViewModifier {
func body(content: Content) -> some View {
content
}
}
extension View {
func TestModifierView() -> some View{
return self.modifier(BlankModifier())
}
}
How to apply TestModifierView only in case of modifierEnabled == true ?

#available(OSX 11.0, *)
public extension View {
#ViewBuilder
func `if`<Content: View>(_ condition: Bool, content: (Self) -> Content) -> some View {
if condition {
content(self)
} else {
self
}
}
}
#available(OSX 11.0, *)
public extension View {
#ViewBuilder
func `if`<TrueContent: View, FalseContent: View>(_ condition: Bool, ifTrue trueContent: (Self) -> TrueContent, else falseContent: (Self) -> FalseContent) -> some View {
if condition {
trueContent(self)
} else {
falseContent(self)
}
}
}
usage example ( one modifier ) :
Text("some Text")
.if(modifierEnabled) { $0.foregroundColor(.Red) }
usage example2 (two modifier chains related to condition) :
Text("some Text")
.if(modifierEnabled) { $0.foregroundColor(.red) }
else: { $0.foregroundColor(.blue).background(Color.green) }
BUT!!!!!!!!!!!
Important thing that this modifier can be reason of some indentity issues. (later you will understand this)
So in some cases better to use standard if construction

I like the solution without type erasers. It looks strict and elegant.
public extension View {
#ViewBuilder
func modify<TrueContent: View, FalseContent: View>(_ condition: Bool, ifTrue modificationForTrue: (Self) -> TrueContent, ifFalse modificationForFalse: (Self) -> FalseContent) -> some View {
if condition {
modificationForTrue(self)
} else {
modificationForFalse(self)
}
}
}
Usage
HStack {
...
}
.modify(modifierEnabled) { v in
v.font(.title)
} ifFalse: {
$0.background(Color.red) // even shorter
}
If you only plan to apply a modifier (or a chain of modifiers) consider this:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
#ViewBuilder func modifier<VM1: ViewModifier, VM2: ViewModifier>(_ condition: #autoclosure () -> Bool, applyIfTrue: VM1, applyIfFalse: VM2
) -> some View {
if condition() {
self.modifier(applyIfTrue)
} else {
self.modifier(applyIfFalse)
}
}
}
Usage is almost as simple as with regular .modifier.
...
Form {
HStack {
...
}
.modifier(modifierEnabled, applyIfTrue: CornerRotateModifier(amount: 8, anchor: .bottomLeading), applyIfFalse: EmptyModifier())
...
You can omit applyIfFalse part for conciseness and just return self.erase() if condition is false.

Related

SwiftUI: In SplitView, how can I detect if the master view is visible?

When SwiftUI creates a SplitView, it adds a toolbar button that hides/shows the Master view. How can I detect this change so that I can resize the font in the detail screen and use all the space optimally?
I've tried using .onChange with geometry but can't seem to get that to work.
If you're using iOS 16 you can use NavigationSplitView with NavigationSplitViewVisibility
Example:
struct MySplitView: View {
#State private var columnVisibility: NavigationSplitViewVisibility = .all
var bothAreShown: Bool { columnVisibility != .detailOnly }
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
Text("Master Column")
} detail: {
Text("Detail Column")
Text(bothAreShown ? "Both are shown" : "Just detail shown")
}
}
}
After thinkering for a while on this I got to this solution:
struct ContentView: View {
#State var isOpen = true
var body: some View {
NavigationView {
VStack{
Text("Primary")
.onUIKitAppear {
isOpen.toggle()
}
.onAppear{
print("hello")
isOpen.toggle()
}
.onDisappear{
isOpen.toggle()
print("hello: bye")
}
.navigationTitle("options")
}
Text("Secondary").font(isOpen ? .body : .title)
}.navigationViewStyle(.columns)
}
}
The onUIKitAppear is a custom extension suggested by apple to be only executed once the view has been presented to the user https://developer.apple.com/forums/thread/655338?page=2
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.delegate = context.coordinator
return vc
}
func makeCoordinator() -> Coordinator {
Coordinator(action: self.action)
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {}
class Coordinator: ActionRepresentable {
var action: () -> Void
init(action: #escaping () -> Void) {
self.action = action
}
func remoteAction() {
action()
}
}
}
protocol ActionRepresentable: AnyObject {
func remoteAction()
}
class UIAppearViewController: UIViewController {
weak var delegate: ActionRepresentable?
var savedView: UIView?
override func viewDidLoad() {
self.savedView = UILabel()
if let _view = self.savedView {
view.addSubview(_view)
}
}
override func viewDidAppear(_ animated: Bool) {
delegate?.remoteAction()
}
override func viewDidDisappear(_ animated: Bool) {
view.removeFromSuperview()
savedView?.removeFromSuperview()
}
}
public extension View {
func onUIKitAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}

In SwiftUI, how to implement something like `navigationBarItems(...)`

I've had some problems with SwiftUI's navigation API, so I'm experimenting with implementing my own. Parts of this are relatively easy: I create a class NavModel that is basically a stack. Depending on what's on the top of that stack, I can display different views.
But I can't see how to implement something like SwiftUI's .navigationBarItems(...). That view modifier seems to use something like the Preferences API to pass its argument View up the hierarchy to the containing navigation system. Eg:
VStack {
...
}.navigationBarItems(trailing: Button("Edit") { startEdit() })
Anything that goes through onPreferenceChange(...) has to be Equatable, so if I want to pass an AnyView? for the navigation bar items, I need to somehow may it Equatable, and I don't see how to do that.
Here's some sample code that shows a basic push and pop navigation. I'm wondering: how could I make the navBarItems(...) work? (The UI is ugly, but that's not important now.)
struct ContentView: View {
#StateObject var navModel: NavModel = .shared
var body: some View {
NavView(model: navModel) { node in
switch node {
case .root: rootView
case .foo: fooView
}
}
}
var rootView: some View {
VStack {
Text("This is the root")
Button {
navModel.push(.foo)
} label: {
Text("Push a view")
}
}
}
var fooView: some View {
VStack {
Text("Foo")
Button {
navModel.pop()
} label: {
Text("Pop nav stack")
}
}.navBarItems(trailing: Text("Test"))
}
}
struct NavView<Content: View>: View {
#ObservedObject var model: NavModel
let makeViews: (NavNode) -> Content
init(model: NavModel, #ViewBuilder makeViews: #escaping (NavNode) -> Content) {
self.model = model
self.makeViews = makeViews
}
#State var navItems: AnyView? = nil
var body: some View {
VStack {
let node = model.stack.last!
navBar
Divider()
makeViews(node)
.frame(maxHeight: .infinity)
// This doesn't compile
.onPreferenceChange(NavBarItemsPrefKey.self) { v in
navItems = v
}
}
}
var navBar: some View {
HStack {
if model.stack.count > 1 {
Button {
model.pop()
} label: { Text("Back") }
}
Spacer()
if let navItems = self.navItems {
navItems
}
}
}
}
enum NavNode {
case root
case foo
}
class NavModel: ObservableObject {
static let shared = NavModel()
#Published var stack: [NavNode]
init() {
stack = [.root]
}
func push(_ node: NavNode) { stack.append(node) }
func pop() {
if stack.count > 1 {
stack.removeLast()
}
}
}
struct NavBarItemsPrefKey: PreferenceKey {
typealias Value = AnyView?
static var defaultValue: Value = nil
static func reduce(value: inout Value, nextValue: () -> Value) {
let n = nextValue()
if n != nil { // ???
value = n
}
}
}
// Is this the right way? But then anything passed to navBarItems(...) would need
// to be Equatable. The common case - Buttons - are not.
struct AnyEquatableView: Equatable {
???
init<T>(_ ev: EquatableView<T>) {
???
}
static func == (lhs: AnyEquatableView, rhs: AnyEquatableView) -> Bool {
???
}
}
struct NavBarItemsModifier<T>: ViewModifier where T: View {
let trailing: T
func body(content: Content) -> some View {
content.preference(key: NavBarItemsPrefKey.self, value: AnyView(trailing))
}
}
extension View {
func navBarItems<T>(trailing: T) -> some View where T: View {
return self.modifier(NavBarItemsModifier(trailing: trailing))
}
}

Conditional modifiers with #available not executing correctly

I've got a custom modifier to replace navigation bar title with an image view, in iOS 14 this is pretty straightforward with the .toolbar modifier, however in iOS 13 it needs a bit more work but it's possible.
The problem comes when I want to use both solutions in a conditional modifier, the following code reproduces the issue, it works when running on iOS 14 but it produces no result on iOS 13, however if the "#available" condition is removed from the modifier leaving only iOS 13 code, it works as expected. Wrapping iOS 14 in AnyView does not help either:
extension View {
#ViewBuilder
func configuresIcon() -> some View {
if #available(iOS 14.0, *){
self.modifier(NavigationConfigurationView14Modifier())
} else {
self.modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
}))
}
}
}
struct NavigationConfigurationViewModifier: ViewModifier {
let configure: (UINavigationBar) -> ()
func body(content: Content) -> some View {
content.background(NavigationControllerLayout(configure: {
configure($0.navigationBar)
}))
}
}
#available(iOS 14.0, *)
struct NavigationConfigurationView14Modifier: ViewModifier {
func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: .principal) {
Image(IMAGE_NAME_HERE)
}
}
}
}
struct NavigationControllerLayout: UIViewControllerRepresentable {
var configure: (UINavigationController) -> () = { _ in }
func makeUIViewController(
context: UIViewControllerRepresentableContext<NavigationControllerLayout>
) -> UIViewController {
UIViewController()
}
func updateUIViewController(
_ uiViewController: UIViewController,
context: UIViewControllerRepresentableContext<NavigationControllerLayout>
) {
if let navigationContoller = uiViewController.navigationController {
configure(navigationContoller)
}
}
}
Try to wrap content in Group (not tested - only idea)
extension View {
#ViewBuilder
func configuresIcon() -> some View {
Group {
if #available(iOS 14.0, *){
self.modifier(NavigationConfigurationView14Modifier())
} else {
self.modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
}))
}
}
}
}
Maybe it will work if we check OS version at runtime, not at compiletime (because buildlimitedavailability(_:) is itself available only since iOS 14).
extension View {
func erase() -> AnyView {
return AnyView(self)
}
func applyIf<VM1: ViewModifier, VM2: ViewModifier>(_ condition: #autoclosure () -> Bool, ApplyIfTrue: VM1, ApplyIfFalse: VM2
) -> AnyView {
if condition() {
return self.modifier(ApplyIfTrue).erase()
} else {
return self.modifier(ApplyIfFalse).erase()
}
}
#ViewBuilder func configuresIcon() -> some View {
self.applyIf(NSProcessInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion(majorVersion: 14, minorVersion: 0, patchVersion: 0)),
ApplyIfTrue: NavigationConfigurationView14Modifier(),
ApplyIfFalse: modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
})))
}
}
If you really need #available option there are several things to try. 1) Maybe it is because of inappropriate extension point, so try to move it out of View's extension. 2)Move the logic from modifiers to the body.
For example the code below work fine.
#available(macOS 10.15, iOS 13.0, *)
struct ContentView: View {
var body: some View {
ScrollView {
if #available(macOS 11.0, iOS 14.0, *) {
LazyVStack {
ForEach(1...1000, id: \.self) { value in
Text("Row \(value)")
}
}
} else {
VStack {
ForEach(1...1000, id: \.self) { value in
Text("Row \(value)")
}
}
}
}
}
}

Detect touch outside Button in SwiftUI

I have a reset button that asks for confirmation first. I would like to set isSure to false is the user touches outside the component.
Can I do this from the Button component?
Here is my button:
struct ResetButton: View {
var onConfirmPress: () -> Void;
#State private var isSure: Bool = false;
var body: some View {
Button(action: {
if (self.isSure) {
self.onConfirmPress();
self.isSure.toggle();
} else {
self.isSure.toggle();
}
}) {
Text(self.isSure ? "Are you sure?" : "Reset")
}
}
}
here is one way to do it:
struct ContentView: View {
var onConfirmPress: () -> Void
#State private var isSure: Bool = false
var body: some View {
GeometryReader { geometry in
ZStack {
// a transparent rectangle under everything
Rectangle()
.frame(width: geometry.size.width, height: geometry.size.height)
.opacity(0.001) // <--- important
.layoutPriority(-1)
.onTapGesture {
self.isSure = false
print("---> onTapGesture self.isSure : \(self.isSure)")
}
Button(action: {
if (self.isSure) {
self.onConfirmPress()
}
self.isSure.toggle()
}) {
Text(self.isSure ? "Are you sure?" : "Reset").padding(10).border(Color.black)
}
}
}
}
}
Basically, we have some view, and we want a tap on its background to do something - meaning, we want to add a huge background that registers a tap. Note that .background is only offered the size of the main view, but can always set an explicit different size! If you know your size that's great, otherwise UIScreen could work...
This is hacky but seems to work!
extension View {
#ViewBuilder
private func onTapBackgroundContent(enabled: Bool, _ action: #escaping () -> Void) -> some View {
if enabled {
Color.clear
.frame(width: UIScreen.main.bounds.width * 2, height: UIScreen.main.bounds.height * 2)
.contentShape(Rectangle())
.onTapGesture(perform: action)
}
}
func onTapBackground(enabled: Bool, _ action: #escaping () -> Void) -> some View {
background(
onTapBackgroundContent(enabled: enabled, action)
)
}
}
Usage:
SomeView()
.onTapBackground(enabled: isShowingAlert) {
isShowingAlert = false
}
This can be easily changed to take a binding:
func onTapBackground(set value: Binding<Bool>) -> some View {
background(
onTapBackgroundContent(enabled: value.wrappedValue) { value.wrappedValue = false }
)
}
// later...
SomeView()
.onTapBackground(set: $isShowingAlert)

use of fixedSIze in SwiftUI

i have made a View extension to make fixedSize more flexible.
Here it is.
It works fine, but i am not sure whether there is an easier way to implement this...?
#available(iOS 13.0, *)
struct FixedSizeView<Content> : View where Content : View {
var content: Content
var on: Bool
public init(_ on: Bool, #ViewBuilder content: () -> Content) {
self.content = content()
self.on = on
}
var body : some View {
Group {
if on {
content.fixedSize()
} else {
content
}
}
}
}
#available(iOS 13.0, *)
extension View {
func fixedSize(active: Bool) -> FixedSizeView<Self> {
FixedSizeView(active) {
self
}
}
}
Why don't make it simpler, as this
extension View {
func fixedSize(active: Bool) -> some View {
Group {
if active {
self.fixedSize()
} else {
self
}
}
}
}
Tested & works with Xcode 11.2 / iOS 13.2