Appropriate extension for this use case - swift

Originally I was looking to make an extension on Text for example:
extension Text {
var headerText: Text {
self
.bold()
.foregroundColor(.blue)
.padding() //<-- Doesn't work
}
}
and it all worked except for padding
So I had the bright idea of writing an extension on View instead but then the .bold() wouldn't work..
Looking for a more swifty way of doing this. Thanks

If I'm understanding correctly, this seems like the perfect case for a custom view modifier...
struct HeaderText: ViewModifier {
func body(content: Content) -> some View {
content
.bold()
.foregroundColor(.blue)
.padding()
}
}
...which you could then use like this:
struct ContentView: View {
var body: some View {
Text("This is a header")
.modifier(HeaderText())
}
}
You could also put the modifier inside a view extension to make it cleaner, like so:
extension View {
func headerText() -> ModifiedContent<Self, HeaderText> {
return modifier(HeaderText())
}
}
That would enable you to use it like this:
struct ContentView: View {
var body: some View {
Text("This is a header")
.headerText()
}
}

Related

SwiftUI ViewModifier not working as a NavigationLink

I have the following ViewModifier and it does not work:
import SwiftUI
struct NavLink: ViewModifier {
let title: String
func body(content: Content) -> some View {
NavigationLink(destination: content) {
Text(title)
}
}
}
extension View {
func navLink(title: String) -> some View {
modifier(NavLink(title: title))
}
}
If I use that as follows:
import SwiftUI
struct MainScreen: View {
var body: some View {
NavigationStack() {
VStack {
// This does not work
NavStackScreen()
.navLink(title: "Nav Stack")
// But this does
Helper.linked(to: NavStackScreen(), title: "Nav Stack 2")
}
}
}
}
struct Helper {
static func linked(to destination: some View, title: String) -> some View {
NavigationLink(destination: destination) {
Text(title)
}
}
}
It creates the link and pushes a new view onto the screen if tapped; however, the contents of the NavStackScreen are not displayed, only an empty screen.
Any ideas about what is going on?
Contents of NavStackScreen for reference:
struct NavStackScreen: View {
var body: some View {
Text("Nav Stack Screen")
.font(.title)
.navigationTitle("Navigation Stack")
}
}
If I use a static helper function within a helper struct, then it works correctly:
static func linked(to destination: some View, title: String) -> some View {
NavigationLink(destination: destination) {
Text(title)
}
}
I added the full code of the MainView for reference and updated the example with more detail for easy reproduction.
The modifier doesn't work because the content argument is not the actual view being modified, but instead is a proxy:
content is a proxy for the view that will have the modifier represented by Self applied to it.
Reference.
This is what a quick debugging over the modifier shows:
(lldb) po content
SwiftUI._ViewModifier_Content<SwiftUIApp.NavLink>()
As the proxy is an internal type of SwiftUI, we can't know for sure why NavigationLink doesn't work with it.
A workaround would be to skip the modifier, and only add the extension over View:
extension View {
func navLink(title: String) -> some View {
NavigationLink(destination: content) {
Text(title)
}
}
}

NavigationView frame glitch with safearea

I try to create a custom ViewModifier similar to SwiftUI's .sheet modifier.
When I try to make a NavigationView spring from bottom, the frame of the view just glitched over safearea. The frame looks as if adjust to the safearea when the view moves from bottom to top.
Anyone knows maybe how to constrain the view frame inside the navigation view to avoid this?
Here is what happened. When click the plus button, the SwiftUI .sheet modifier shows up. Custom popup shows up when pressing the gear button.
Problem gif recording here
Here is code of the custom popup view.
struct SettingsView: View {
#Binding var showingSelf: Bool
#Binding var retryWrongCards: Bool
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
Section {
Toggle(isOn: $retryWrongCards) {
Text("Retry Wrong Cards")
}
}
}
.animation(nil)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationBarTitle("Settings")
.navigationBarItems(trailing: Button("Done") {
self.showingSelf = false
})
}
}
}
}
Here's the code of custom modifier
struct Popup<T: View>: ViewModifier {
let popup: T
let isPresented: Bool
init(isPresented: Bool, #ViewBuilder content: () -> T) {
self.isPresented = isPresented
popup = content()
}
func body(content: Content) -> some View {
content
.overlay(popupContent())
}
#ViewBuilder private func popupContent() -> some View {
GeometryReader { geometry in
if isPresented {
popup
.animation(.spring())
.transition(.offset(x: 0, y: geometry.belowScreenEdge))
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
}
private extension GeometryProxy {
var belowScreenEdge: CGFloat {
UIScreen.main.bounds.height - frame(in: .global).minY
}
}
After debugging and trying many changes, I was already preparing a mini-project to file a SwiftUI bug for Apple. In the end, it was not necessary and we have a simple solution 🥳!!
TLDR; your SettingsView does not correctly initialise the NavigationView.
The modifier .navigationViewStyle(StackNavigationViewStyle()) has to be applied onto the NavigationView and not inside it (in contrast to navigationBarTitle or navigationBarItems which only work when put inside the NavigationView):
NavigationView {
List {
Section {
Toggle(isOn: $retryWrongCards) {
Text("Retry Wrong Cards")
}
}
}
.animation(nil)
.listStyle(GroupedListStyle())
.navigationBarTitle("Settings")
.navigationBarItems(trailing: Button("Done") {
self.showingSelf = false
})
}
.navigationViewStyle(StackNavigationViewStyle())
After this change your custom sheet modifier behaves for me identically as the regular .sheet modifier! Great work!
Philipp
It seems your animation does exactly what a sheet does. I would just replace it with that. You can easily present a sheet on top of a sheet.
Edit
After you posted your ViewModifier code, it seems the only difference is the animation type and the size of the popup.

Creating BaseView class in SwiftUI

Lately started learning/developing apps with SwiftUI and seems pretty easy to build the UI components. However, struggling creating a BaseView in SwiftUI. My idea is to have the common UI controls like background , navigation , etc in BaseView and just subclass other SwiftUI views to have the base components automatically.
Usually you want to either have a common behaviour or a common style.
1) To have a common behaviour: composition with generics
Let's say we need to create a BgView which is a View with a full screen image as background. We want to reuse BgView whenever we want. You can design this situation this way:
struct BgView<Content>: View where Content: View {
private let bgImage = Image.init(systemName: "m.circle.fill")
let content: Content
var body : some View {
ZStack {
bgImage
.resizable()
.opacity(0.2)
content
}
}
}
You can use BgView wherever you need it and you can pass it all the content you want.
//1
struct ContentView: View {
var body: some View {
BgView(content: Text("Hello!"))
}
}
//2
struct ContentView: View {
var body: some View {
BgView(content:
VStack {
Text("Hello!")
Button(action: {
print("Clicked")
}) {
Text("Click me")
}
}
)
}
}
2) To have a common behaviour: composition with #ViewBuilder closures
This is probably the Apple preferred way to do things considering all the SwiftUI APIs. Let's try to design the example above in this different way
struct BgView<Content>: View where Content: View {
private let bgImage = Image.init(systemName: "m.circle.fill")
private let content: Content
public init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body : some View {
ZStack {
bgImage
.resizable()
.opacity(0.2)
content
}
}
}
struct ContentView: View {
var body: some View {
BgView {
Text("Hello!")
}
}
}
This way you can use BgView the same way you use a VStack or List or whatever.
3) To have a common style: create a view modifier
struct MyButtonStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
.shadow(radius: 3)
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Button(action: {
print("Button1 clicked")
}) {
Text("Button 1")
}
.modifier(MyButtonStyle())
Button(action: {
print("Button2 clicked")
}) {
Text("Button 2")
}
.modifier(MyButtonStyle())
Button(action: {
print("Button3 clicked")
}) {
Text("Button 3")
}
.modifier(MyButtonStyle())
}
}
}
These are just examples but usually you'll find yourself using one of the above design styles to do things.
EDIT: a very useful link about #functionBuilder (and therefore about #ViewBuilder) https://blog.vihan.org/swift-function-builders/
I got a idea about how to create a BaseView in SwiftUI for common usage in other screen
By the way
Step .1 create ViewModifier
struct BaseScene: ViewModifier {
/// Scene Title
var screenTitle: String
func body(content: Content) -> some View {
VStack {
HStack {
Spacer()
Text(screenTitle)
.font(.title)
.foregroundColor(.white)
Spacer()
}.padding()
.background(Color.blue.opacity(0.8))
content
}
}
}
Step .2 Use that ViewModifer in View
struct BaseSceneView: View {
var body: some View {
VStack {
Spacer()
Text("Home screen")
.font(.title)
Spacer()
}
.modifier(BaseScene(screenTitle: "Screen Title"))
}
}
struct BaseSceneView_Previews: PreviewProvider {
static var previews: some View {
Group {
BaseSceneView()
}
}
}
Your Output be like:

How to pass one SwiftUI View as a variable to another View struct

I'm implementing a very custom NavigationLink called MenuItem and would like to reuse it across the project. It's a struct that conforms to View and implements var body : some View which contains a NavigationLink.
I need to somehow store the view that shall be presented by NavigationLink in the body of MenuItem but have yet failed to do so.
I have defined destinationView in MenuItem's body as some View and tried two initializers:
This seemed too easy:
struct MenuItem: View {
private var destinationView: some View
init(destinationView: View) {
self.destinationView = destinationView
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
2nd try:
struct MenuItem: View {
private var destinationView: some View
init<V>(destinationView: V) where V: View {
self.destinationView = destinationView
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Cannot assign value of type 'V' to type 'some View'.
Final try:
struct MenuItem: View {
private var destinationView: some View
init<V>(destinationView: V) where V: View {
self.destinationView = destinationView as View
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Cannot assign value of type 'View' to type 'some View'.
I hope someone can help me. There must be a way if NavigationLink can accept some View as an argument.
Thanks ;D
To sum up everything I read here and the solution which worked for me:
struct ContainerView<Content: View>: View {
#ViewBuilder var content: Content
var body: some View {
content
}
}
This not only allows you to put simple Views inside, but also, thanks to #ViewBuilder, use if-else and switch-case blocks:
struct SimpleView: View {
var body: some View {
ContainerView {
Text("SimpleView Text")
}
}
}
struct IfElseView: View {
var flag = true
var body: some View {
ContainerView {
if flag {
Text("True text")
} else {
Text("False text")
}
}
}
}
struct SwitchCaseView: View {
var condition = 1
var body: some View {
ContainerView {
switch condition {
case 1:
Text("One")
case 2:
Text("Two")
default:
Text("Default")
}
}
}
}
Bonus:
If you want a greedy container, which will claim all the possible space (in contrary to the container above which claims only the space needed for its subviews) here it is:
struct GreedyContainerView<Content: View>: View {
#ViewBuilder let content: Content
var body: some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
If you need an initializer in your view then you can use #ViewBuilder for the parameter too. Even for multiple parameters if you will:
init(#ViewBuilder content: () -> Content) {…}
The way Apple does it is using function builders. There is a predefined one called ViewBuilder. Make it the last argument, or only argument, of your init method for MenuItem, like so:
..., #ViewBuilder builder: #escaping () -> Content)
Assign it to a property defined something like this:
let viewBuilder: () -> Content
Then, where you want to diplay your passed-in views, just call the function like this:
HStack {
viewBuilder()
}
You will be able to use your new view like this:
MenuItem {
Image("myImage")
Text("My Text")
}
This will let you pass up to 10 views and use if conditions etc. though if you want it to be more restrictive you will have to define your own function builder. I haven't done that so you will have to google that.
You should make the generic parameter part of MenuItem:
struct MenuItem<Content: View>: View {
private var destinationView: Content
init(destinationView: Content) {
self.destinationView = destinationView
}
var body : some View {
// ...
}
}
You can create your custom view like this:
struct ENavigationView<Content: View>: View {
let viewBuilder: () -> Content
var body: some View {
NavigationView {
VStack {
viewBuilder()
.navigationBarTitle("My App")
}
}
}
}
struct ENavigationView_Previews: PreviewProvider {
static var previews: some View {
ENavigationView {
Text("Preview")
}
}
}
Using:
struct ContentView: View {
var body: some View {
ENavigationView {
Text("My Text")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can pass a NavigationLink (or any other view widget) as a variable to a subview as follows:
import SwiftUI
struct ParentView: View {
var body: some View {
NavigationView{
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
ChildView(destinationView: ThirdView(), title: "3rd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
}
struct ChildView<Content: View>: View {
var destinationView: Content
var title: String
init(destinationView: Content, title: String) {
self.destinationView = destinationView
self.title = title
}
var body: some View {
NavigationLink(destination: destinationView){
Text("This item opens the \(title) view").foregroundColor(Color.black)
}
}
}
struct ThirdView: View {
var body: some View {
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
ChildView(destinationView: ThirdView(), title: "3rd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
The accepted answer is nice and simple. The syntax got even cleaner with iOS 14 + macOS 11:
struct ContainerView<Content: View>: View {
#ViewBuilder var content: Content
var body: some View {
content
}
}
Then continue to use it like this:
ContainerView{
...
}
I really struggled to make mine work for an extension of View. Full details about how to call it are seen here.
The extension for View (using generics) - remember to import SwiftUI:
extension View {
/// Navigate to a new view.
/// - Parameters:
/// - view: View to navigate to.
/// - binding: Only navigates when this condition is `true`.
func navigate<SomeView: View>(to view: SomeView, when binding: Binding<Bool>) -> some View {
modifier(NavigateModifier(destination: view, binding: binding))
}
}
// MARK: - NavigateModifier
fileprivate struct NavigateModifier<SomeView: View>: ViewModifier {
// MARK: Private properties
fileprivate let destination: SomeView
#Binding fileprivate var binding: Bool
// MARK: - View body
fileprivate func body(content: Content) -> some View {
NavigationView {
ZStack {
content
.navigationBarTitle("")
.navigationBarHidden(true)
NavigationLink(destination: destination
.navigationBarTitle("")
.navigationBarHidden(true),
isActive: $binding) {
EmptyView()
}
}
}
}
}
Alternatively you can use a static function extension. For example, I make a titleBar extension to Text. This makes it very easy to reuse code.
In this case you can pass a #Viewbuilder wrapper with the view closure returning a custom type that conforms to view. For example:
import SwiftUI
extension Text{
static func titleBar<Content:View>(
titleString:String,
#ViewBuilder customIcon: ()-> Content
)->some View {
HStack{
customIcon()
Spacer()
Text(titleString)
.font(.title)
Spacer()
}
}
}
struct Text_Title_swift_Previews: PreviewProvider {
static var previews: some View {
Text.titleBar(titleString: "title",customIcon: {
Image(systemName: "arrowshape.turn.up.backward")
})
.previewLayout(.sizeThatFits)
}
}
If anyone is trying to pass two different views to other view, and can't do it because of this error:
Failed to produce diagnostic for expression; please submit a bug report...
Because we are using <Content: View>, the first view you passed, the view is going to store its type, and expect the second view you are passing be the same type, this way, if you want to pass a Text and an Image, you will not be able to.
The solution is simple, add another content view, and name it differently.
Example:
struct Collapsible<Title: View, Content: View>: View {
#State var title: () -> Title
#State var content: () -> Content
#State private var collapsed: Bool = true
var body: some View {
VStack {
Button(
action: { self.collapsed.toggle() },
label: {
HStack {
self.title()
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
VStack {
self.content()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none)
.clipped()
.animation(.easeOut)
.transition(.slide)
}
}
}
Calling this View:
Collapsible {
Text("Collapsible")
} content: {
ForEach(1..<5) { index in
Text("\(index) test")
}
}
Syntax for 2 Views
struct PopOver<Content, PopView> : View where Content: View, PopView: View {
var isShowing: Bool
#ViewBuilder var content: () -> Content
#ViewBuilder var popover: () -> PopView
var body: some View {
ZStack(alignment: .center) {
self
.content()
.disabled(isShowing)
.blur(radius: isShowing ? 3 : 0)
ZStack {
self.popover()
}
.frame(width: 112, height: 112)
.opacity(isShowing ? 1 : 0)
.disabled(!isShowing)
}
}
}

Cannot use custom view in SwiftUI

For my SwiftUI application, I've created a simple Title view, that has a set font size and text color. Title is declared as follows:
struct Title: View {
var string: String
var body: some View {
Text(string)
.font(.system(size: 32))
.color(Color.black)
}
}
I have the following text objects in my content view's body right now:
var body: some View {
VStack(alignment: .leading) {
Text("Welcome")
.font(.largeTitle)
.color(Color.black)
Text("to SwiftUI")
.font(.largeTitle)
.color(Color.secondary)
}
}
So now, I want to replace these two Texts with my Titles:
var body: some View {
VStack(alignment: .leading) {
Title("Welcome")
Title("to SwiftUI")
}
}
After replacing the views, I'm getting some seemingly unrelated error messages from Xcode, that stop the application from compiling:
Static member 'leading' cannot be used on instance of type 'HorizontalAlignment'
'(LocalizedStringKey) -> Text' is not convertible to '(LocalizedStringKey, String?, Bundle?, StaticString?) -> Text'
'Font' is not convertible to 'Font?'
...and more. Reverting back to Text instead of Title "fixes" the issues.
What's interesting is that I also have a custom PrimaryButton view that I was able to add without any issues:
struct PrimaryButton: View {
var title: String
var body: some View {
Button(action: { print("tapped") }) {
Text(title)
.font(Font.primaryButton)
.offset(y: 1)
.padding(.horizontal, 20)
.padding(.vertical, 14)
}
}
}
...and then using it:
PrimaryButton(title: "Let's go")
Question
Is this simply a beta-issue, or am I missing something?
You need to add string: to your Title() initializer:
var body: some View {
VStack(alignment: .leading) {
Title(string: "Welcome")
Title(string: "to SwiftUI")
}
}
Compiler errors are currently misleading and not located near where the real issue is.
You are missing the string: param in the initializer.
Please find the updated code below:
var body: some View {
VStack(alignment: .leading) {
Title(string: "Welcome")
Title(string: "to SwiftUI")
}
}
FYI:
I have created one sample application
// MARK - CustomView
struct ContentView : View {
var body: some View {
VStack{
CustomView(aString: "First String")
CustomView(aString: "Second String")
}
}
}
// MARK - CustomView
struct CustomView : View {
var aString: String
var body: some View {
Text(aString)
}
}
Today, 01oct2019, Swift prompted me to replace string: with. verbatim: .
Text(verbatim: "Pressure") works today
Text(string: "Pressure") did work yesterday but not today.
hth