Transparent navigationbar in SwiftUI with backgroundImage - ios

I have a custom navigation bar that is the an image followed by the header text. The image is set to scale to fill but does not quite fill up the navigation bar. As such, you can see a little of the bar where the image does not cover. I've tried to set the background color of the nav bar to clear but that doesn't work. Any suggestions?
struct ContentView: View {
#State private var hideBar = true
var body: some View {
VStack {
NavigationView {
ZStack {
Image("bg5").resizable().scaledToFill()
VStack {
NavigationLink(destination:
SubView(header: "Go to subview")) {
Text("Go to subview")
}.simultaneousGesture(TapGesture().onEnded {
self.hideBar = false // << show, here to be smooth !!
})
NavigationLink(destination:
SubView(header: "Go again")) {
Text("Go to subview again")
}.simultaneousGesture(TapGesture().onEnded {
self.hideBar = false // << show, here to be smooth !!
})
}
.navigationBarTitle("")
.navigationBarHidden(hideBar)
.onAppear {
self.hideBar = true // << hide on back
}
}.edgesIgnoringSafeArea([.top, .bottom])
}
}
}
}
struct SubView: View {
var header: String
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var btnBack : some View { Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("subheaderback").resizable().scaledToFit()
VStack(alignment: .center) {
Text(self.header)
}.frame(width: 100, height: 100)
}
}.buttonStyle(PlainButtonStyle())
}
var body: some View {
ZStack {
Image("bg5").resizable().scaledToFill()
VStack {
Text("blah blah")
Text("and more blah")
}
}.edgesIgnoringSafeArea([.top, .bottom])
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}
extension UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .clear
appearance.backgroundImage = UIImage(named: "subheader")
navigationBar.standardAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
}
}

Instead of setting background color you need different configuration, like below (tested with Xcode 11.4 / iOS 13.4)
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground() // << this one !!
appearance.backgroundImage = UIImage(named: "subheader")
// appearance.backgroundImageContentMode = .scaleAspectFit // if needed non-default

Related

SwiftUI: Wrong TabView's Tab Bar height in landscape mode

This is a simple example of programmatic Tab Bar height calculation for TabView:
import SwiftUI
struct ContentView: View {
let tabbarHeight = UITabBarController().tabBar.frame.height
var body: some View {
TabView {
VStack {
Text("First View")
Text("tabbar height: \(tabbarHeight)")
}
.tabItem {
Label("First", systemImage: "house")
}
Text("Second View")
.tabItem {
Label("Second", systemImage: "chart.bar")
}
}
.overlay {
VStack {
Spacer()
Rectangle().fill(.green)
.frame(width: 50, height: tabbarHeight)
}
}
.onAppear {
let tabBarAppearance = UITabBarAppearance()
UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
tabBarAppearance.backgroundColor = .purple
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Everything is ok When running in portrait mode:
But when launching in landscape mode, tabar height is being calculated wrong (49 instead of 32):
Is it a bug?
Xcode 14.1/iPhone 13/iOS 16.1

How to set a UISegmentedControl and SwiftUI shape to the same color?

I have created a RoundedRectangle and set its foregroundColor to blue
RoundedRectangle(cornerRadius: 10).foregroundColor(.blue)
I have created a custom SegmentedControl struct and set its color to blue
UISegmentedControl.appearance().selectedSegmentTintColor = .blue
However, the objects render as totally different shades of blue. How can I set them to the same blue color?
Full code:
import SwiftUI
struct ContentView: View {
#State var choice = Choice.a
var body: some View {
VStack {
Menu { } label: {
Text("Menu")
.foregroundColor(.white)
.padding(5)
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.blue)
)
}
SegmentedControl(choice: $choice)
.padding(.horizontal, 50)
}
}
}
struct SegmentedControl: View {
#Binding var choice: Choice
var choices = [Choice.a, Choice.b]
init(choice: Binding<Choice>) {
UISegmentedControl.appearance().selectedSegmentTintColor = .blue
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .normal)
self._choice = choice
}
var body: some View {
Picker("Which tab?",
selection: $choice,
content: {
ForEach(choices, id: \.self) {
Text($0.description())
}
})
.pickerStyle(.segmented)
}
}
enum Choice {
case a
case b
func description() -> String {
var str = ""
switch self {
case .a:
str = "Choice A"
case .b:
str = "Choice B"
}
return str
}
}
The difference is that Color.blue uses UIColor.systemBlue
So you can approach it two ways
First, you can change the rectangle
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color(UIColor.blue))
Or second change the control
UISegmentedControl.appearance().selectedSegmentTintColor = .systemBlue
Either of these changes will match the other. It depends which you prefer.
Hi you could create a 'Color Set' in the assets and then call your Color like that :
RoundedRectangle(cornerRadius: 10).foregroundColor(Color("the name of your Color"))
UISegmentedControl.appearance().selectedSegmentTintColor = Color("the same Color")

Enable different States inside of buttons SwiftUI

Problem:
Within a button I have states which do different things for example hide the tools and the navigation but for some reason I can't show the pencil panel after these views are hidden, any ideas?
#State var showPencilPanel:Bool = false
#Binding var showTools:Bool
#Binding var navOpen:Bool
Button(action: {
self.showTools = false
self.navOpen = false
self.showPencilPanel = true
})
{
VStack {
Spacer()
Image("Text Btn").resizable().renderingMode(.original)
.frame(width: 24, height: 24)
Text("Text").font(.system(size: 10.0)).foregroundColor(.black)
Spacer()
}
}
.position(x: 60, y: 640)
}
ZStack {
Color.white.opacity(0).edgesIgnoringSafeArea(.all)
VStack (alignment: .center) {
//Show Pencil
if self.showPencilPanel {
MyCanvas(canvasView: canvasView, selectedImage: Binding(get: { selectedImage }, set: { selectedImage = $0 } ))
}
}
Maybe you should use .zindex for that
MyCanvas(canvasView: canvasView, selectedImage: Binding(get: { selectedImage }, set: { selectedImage = $0 } ))
.zindex(5)
https://developer.apple.com/documentation/swiftui/view/zindex(_:)

Reloading Previous View from Navbar

I have been following a tutorial on creating a weather app. I am trying to take it further. When on the weather view the user can click a plus button which takes them to a location view. Here the user will be able to update the location then when going back the weather will reload. But i am really struggling to get the weather to reload when pressing the back button.
Below is my code for the WeatherView & LocationView
Thank you
struct WeatherView: View {
#State private var isShowing = false
#State var width = UIScreen.main.bounds.width - 60
#State var x = -UIScreen.main.bounds.width + 60
#ObservedObject var input = CityId()
let heptics = UIImpactFeedbackGenerator(style: .medium)
#ObservedObject var weatherViewModel = WeatherViewModel()
var body: some View {
NavigationView{
ZStack {
BackgroundView()
VStack {
if weatherViewModel.stateView == .loading {
ActivityIndicatorView(isAnimating: true).configure {
$0.color = .white
}
}
if weatherViewModel.stateView == .success {
LocationAndTemperatureHeaderView(data: weatherViewModel.currentWeather)
Spacer()
ScrollView(.vertical, showsIndicators: false) {
VStack {
DailyWeatherCellView(data: weatherViewModel.todayWeather)
Rectangle().frame(height: CGFloat(1))
HourlyWeatherView(data: weatherViewModel.hourlyWeathers)
Rectangle().frame(height: CGFloat(1))
DailyWeatherView(data: weatherViewModel.dailyWeathers)
Rectangle().frame(height: CGFloat(1))
Text(weatherViewModel.currentDescription)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(
.init(arrayLiteral:.leading,.trailing),
24
)
Rectangle().frame(height: CGFloat(1))
DetailsCurrentWeatherView(data: weatherViewModel.currentWeather)
Rectangle().frame(height: CGFloat(1))
}
}
Spacer()
}
if weatherViewModel.stateView == .failed {
Button(action: {
self.weatherViewModel.retry()
}) {
Text("Failed get data, retry?")
.foregroundColor(.white)
}
}
}
}.colorScheme(.dark)
.listStyle(InsetGroupedListStyle())
.background(
NavigationLink(destination: LocationView(input: input, weatherViewModel: weatherViewModel), isActive: $isShowing) {
EmptyView()
}
) // End of Background
.navigationBarTitle((input.score), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing){
Button(action: {
isShowing = true
heptics.impactOccurred()
}) {
Image(systemName: "plus")
}
} // End of ToolbarItem
} // End of Toolbar
} // End of Nav View
} // End of body
} // End of View
struct LocationView: View {
#ObservedObject var input: CityId
#ObservedObject var weatherViewModel: WeatherViewModel
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
input.score = "2643743"
weatherViewModel.stateView = .loading
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "plus")
}
.navigationBarBackButtonHidden(true)
}
}
You may consider adding .onAppear modifier to your first view then you can update the data.

Custom modal transitions in SwiftUI

I'm trying to recreate the iOS 11/12 App Store with SwiftUI.
Let's imagine the "story" is the view displayed when tapping on the card.
I've done the cards, but the problem I'm having now is how to do the animation done to display the "story".
As I'm not good at explaining, here you have a gif:
Gif 1
Gif 2
I've thought of making the whole card a PresentationLink, but the "story" is displayed as a modal, so it doesn't cover the whole screen and doesn't do the animation I want.
The most similar thing would be NavigationLink, but that then obliges me to add a NavigationView, and the card is displayed like another page.
I actually do not care whether its a PresentationLink or NavigationLink or whatever as long as it does the animation and displays the "story".
Thanks in advance.
My code:
Card.swift
struct Card: View {
var icon: UIImage = UIImage(named: "flappy")!
var cardTitle: String = "Welcome to \nCards!"
var cardSubtitle: String = ""
var itemTitle: String = "Flappy Bird"
var itemSubtitle: String = "Flap That!"
var cardCategory: String = ""
var textColor: UIColor = UIColor.white
var background: String = ""
var titleColor: Color = .black
var backgroundColor: Color = .white
var body: some View {
VStack {
if background != "" {
Image(background)
.resizable()
.frame(width: 380, height: 400)
.cornerRadius(20)
} else {
RoundedRectangle(cornerRadius: 20)
.frame(width: 400, height: 400)
.foregroundColor(backgroundColor)
}
VStack {
HStack {
VStack(alignment: .leading) {
if cardCategory != "" {
Text(verbatim: cardCategory.uppercased())
.font(.headline)
.fontWeight(.heavy)
.opacity(0.3)
.foregroundColor(titleColor)
//.opacity(1)
}
HStack {
Text(verbatim: cardTitle)
.font(.largeTitle)
.fontWeight(.heavy)
.lineLimit(3)
.foregroundColor(titleColor)
}
}
Spacer()
}.offset(y: -390)
.padding(.bottom, -390)
HStack {
if cardSubtitle != "" {
Text(verbatim: cardSubtitle)
.font(.system(size: 17))
.foregroundColor(titleColor)
}
Spacer()
}
.offset(y: -50)
.padding(.bottom, -50)
}
.padding(.leading)
}.padding(.leading).padding(.trailing)
}
}
So
Card(cardSubtitle: "Welcome to this library I made :p", cardCategory: "CONNECT", background: "flBackground", titleColor: .white)
displays:
SwiftUI doesn't do custom modal transitions right now, so we have to use a workaround.
One method that I could think of is to do the presentation yourself using a ZStack. The source frame could be obtained using a GeometryReader. Then, the destination shape could be controlled using frame and position modifiers.
In the beginning, the destination will be set to exactly match position and size of the source. Then immediately afterwards, the destination will be set to fullscreen size in an animation block.
struct ContentView: View {
#State var isPresenting = false
#State var isFullscreen = false
#State var sourceRect: CGRect? = nil
var body: some View {
ZStack {
GeometryReader { proxy in
Button(action: {
self.isFullscreen = false
self.isPresenting = true
self.sourceRect = proxy.frame(in: .global)
}) { ... }
}
if isPresenting {
GeometryReader { proxy in
ModalView()
.frame(
width: self.isFullscreen ? nil : self.sourceRect?.width ?? nil,
height: self.isFullscreen ? nil : self.sourceRect?.height ?? nil)
.position(
self.isFullscreen ? proxy.frame(in: .global).center :
self.sourceRect?.center ?? proxy.frame(in: .global).center)
.onAppear {
withAnimation {
self.isFullscreen = true
}
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
extension CGRect {
var center : CGPoint {
return CGPoint(x:self.midX, y:self.midY)
}
}
SwiftUI in iOS/tvOS 14 and macOS 11 has matchedGeometryEffect(id:in:properties:anchor:isSource:) to animate view transitions between different hierarchies.
Link to Official Documentation
Here's a minimal example:
struct SomeView: View {
#State var isPresented = false
#Namespace var namespace
var body: some View {
VStack {
Button(action: {
withAnimation {
self.isPresented.toggle()
}
}) {
Text("Toggle")
}
SomeSourceContainer {
MatchedView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: !isPresented)
}
if isPresented {
SomeTargetContainer {
MatchedTargetView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: isPresented)
}
}
}
}
}

Resources