How to extend Navaigation View to show beyond safe area in swiftUI - swiftui

I am trying to use NavigationView and created custom NavigationModifier:
but the navigation bar has some white space at top , i want to cover it all with navigation bar it self.
i tried adding ignoreSafeArea didn't got the result.
Here is my code:
struct HomeScreen: View {
var body: some View {
NavigationView {
VStack(spacing: 10) {
Text("Hello:Home")
.modifier(NavModifier())
}
}
}
}
Modifier:
struct NavModifier : ViewModifier {
init() {
let color = UIColor(Color.appTheme)
UINavigationBar.appearance().backgroundColor = color
}
func body(content: Content) -> some View {
return content
.navigationBarTitleDisplayMode(.inline)
.ignoresSafeArea(edges: .top)
.toolbar {
ToolbarItem(placement: .principal) {
Image(systemName: "person")
}
}
}
}
and result screen:

We need to replace appearance completely, like
struct NavModifier : ViewModifier {
init() {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithOpaqueBackground()
navBarAppearance.backgroundColor = UIColor.yellow // << for test !!
UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
}
// ...
}
Tested with Xcode 13.4 / iOS 15.5

Related

Swiftui - Scrollview starting position [duplicate]

ScrollView(){
//no matter
}
Position is Top Left, but content will have smaller width than I needed
(2 strings in red rectangle must be single string, so need to enable both scrolls )
ScrollView([Axis.Set.horizontal, Axis.Set.vertical]) {
//no matter
}
In this case ScrollView's content have correct size for me.
But position of scrollView's content is centred ( both: horizontally and vertically )
How can I change default position to Top Left in case of ScrollView configured to both scrolls?
Update: Xcode 13.4 / macOS 12.4
The issue is still there, but now the solution is simpler using ScrollViewReader:
struct TestTwoAxisScrollView: View {
var body: some View {
ScrollViewReader { sp in
ScrollView([.horizontal, .vertical]) {
VStack {
ForEach(0..<100) { _ in
self.row()
}
}
.border(Color.green)
.id("root")
}
.border(Color.gray)
.padding()
.onAppear {
sp.scrollTo("root", anchor: .topLeading)
}
}
}
func row() -> some View {
Text(test)
.border(Color.red) // uncomment to see border
}
}
Original
Here is possible approach. Tested with Xcode 11.2 / macOS 10.15.3
Demo:
Code (complete testable module, borders are added for better visibility of each component):
import SwiftUI
let test = """
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12118" systemVersion="16A323" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
"""
struct ScrollViewHelper: NSViewRepresentable {
func makeNSView(context: NSViewRepresentableContext<ScrollViewHelper>) -> NSView {
let view = NSView(frame: .zero)
DispatchQueue.main.async { // << must be async, so view got into view hierarchy
view.enclosingScrollView?.contentView.scroll(to: .zero)
view.enclosingScrollView?.reflectScrolledClipView(view.enclosingScrollView!.contentView)
}
return view
}
func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<ScrollViewHelper>) {
}
}
struct TestTwoAxisScrollView: View {
var body: some View {
ScrollView([.horizontal, .vertical]) {
VStack {
ForEach(0..<100) { _ in
self.row()
}
}
.background(ScrollViewHelper()) // << active part !!
.border(Color.green) // uncomment to see border
}
.border(Color.gray)
.padding()
}
func row() -> some View {
Text(test)
.border(Color.red) // uncomment to see border
}
}
struct TestTwoAxisScrollView_Previews: PreviewProvider {
static var previews: some View {
TestTwoAxisScrollView()
}
}

Additional safe area on NavigationView in SwiftUI

I am building a SwiftUI app where I have an overlay that is conditionally shown across my entire application like this:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
.safeAreaInset(edge: .bottom) {
Group {
if myCondition {
EmptyView()
} else {
OverlayView()
}
}
}
}
}
}
I would expect this to adjust the safe area insets of the NavigationView and propagate it to any content view, so content is not stuck under the overlay. At least that's how additionalSafeAreaInsets in UIKit would behave. Unfortunately, it seems that SwiftUI ignores any safeAreaInsets() on a NavigationView (the overlay will show up, but safe area is not adjusted).
While I can use a GeometryReader to read the overlay size and then set safeAreaInsets() on ContentView, this will only work for ContentView - as soon as I navigate to the next view the safe area is gone.
Is there any nice way to get NavigationView to accept additional safe area insets, either by using safeAreaInsets() or by some other way?
So it seems NavigationView does not adjust its safe area inset when using .safeAreaInset. If this is intended or a bug is not clear to me. Anyway, I solved this for now like this (I wanted to use pure SwiftUI, using UIKit's additionalSafeAreaInsets might be an option to):
Main App File:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
.environmentObject(SafeAreaController.shared)
.safeAreaInset(edge: .bottom) {
OverlayView()
.frameReader(safeAreaController.updateAdditionalSafeArea)
}
}
}
}
class SafeAreaController: ObservableObject {
static let shared = SafeAreaController()
#Published private(set) var additionalSafeArea: CGRect = .zero
func updateAdditionalSafeArea(_ newValue: CGRect) {
if newValue != additionalSafeArea {
additionalSafeArea = newValue
}
}
}
struct FrameReader: ViewModifier {
let changeChandler: ((CGRect) -> Void)
init(_ changeChandler: #escaping (CGRect) -> Void) {
self.changeChandler = changeChandler
}
func body(content: Content) -> some View {
content
.background(
GeometryReader { geometry -> Color in
DispatchQueue.main.async {
let newFrame = geometry.frame(in: .global)
changeChandler(newFrame)
}
return Color.clear
}
)
}
}
extension View {
func frameReader(_ changeHandler: #escaping (CGRect) -> Void) -> some View {
return modifier(FrameReader(changeHandler))
}
}
EVERY Content View that is pushed on your NavigationView:
struct ContentView: View {
#EnvironmentObject var safeAreaController: SafeAreaController
var body: some View {
YourContent()
.safeAreaInset(edge: .bottom) {
Color.clear.frame(height: safeAreaController.additionalSafeArea.height)
}
}
Why does it work?
In the main app file, a GeometryReader is used to read the size of the overlay created inside safeAreaInset(). The size is written to the shared SafeAreaController
The shared SafeAreaController is handed as an EnvironmentObject to every content view of our navigation
An invisible object is created as the .safeAreaInset of every content view with the height read from the SafeAreaController - this will basically create an invisible bottom safe area that is the same size as our overlay, thus making room for the overlay
struct ContentView: View {
var body: some View {
NavigationView {
ListView()
.navigationBarTitle("Test")
}
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
Text("My Overlay")
.padding()
Spacer()
}
.background(.ultraThinMaterial)
}
}
}
struct ListView: View {
var body: some View {
List(0..<30) { item in
NavigationLink {
Text("Next view")
} label: {
Text("Item \(item)")
}
}
}
}

SwiftUI UINavigationBar does not ignore the top safe area. How to get rid of empty space on the top?

Code I used to create an example of this navigation bar:
struct ContentView: View {
init() {
let defaultAppearance = UINavigationBarAppearance()
defaultAppearance.configureWithOpaqueBackground()
defaultAppearance.backgroundColor = .red
UINavigationBar.appearance().standardAppearance = defaultAppearance
UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
}
var body: some View {
ParentView()
}
}
struct ParentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Tap here")
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.all)
}
}
}
struct DetailView: View {
var body: some View {
VStack {
Text("Detail View")
}
.edgesIgnoringSafeArea(.all)
}
}
It doesn't matter where I'm putting .edgesIgnoringSafeArea(.all) it doesn't work. Is there another way to tell Navigation Bar to ignore the safe area?
If it's not the safe area problem, then I need to make the whole navigation bar to have the same height of UINavigationBarContentView displayed in UI Hierarchy:

SwiftUI - How to change the background Color of top safe area to gray?

I want to change the background color of top safe area from green to gray. I have looked everywhere but could not find any solution. The screen in preview looks like this.
My codes:
struct ContentView: View {
#State var name = ""
init() {
//Use this if NavigationBarTitle is with Large Font
UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.red]
UINavigationBar.appearance().backgroundColor = .gray
}
var body: some View {
NavigationView {
ZStack{
VStack{
TextField("Name", text: $name)
.frame(height:200)
.padding()
.background(backgrounImage())
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.gray,lineWidth: 4))
.padding()
Spacer()
}.navigationTitle("Tanvir")
.background(Color.green.edgesIgnoringSafeArea(.all))
}
}
}
}
You can add another view on top of the ZStack:
var body: some View {
NavigationView {
ZStack(alignment: .top) { // <- Don't forget this
,,,
GeometryReader { reader in
Color.yellow
.frame(height: reader.safeAreaInsets.top, alignment: .top)
.ignoresSafeArea()
}
}
}
}
Don't forget the stack alignment!
Consistant Bar for the entire App
If you need it to be on all of your views, try putting the code somewhere more consistent like where you are providing the contentView:
#main
struct SwiftUIAppPlaygroundApp: App {
var body: some Scene {
WindowGroup {
ZStack {
ContentView()
GeometryReader { reader in
Color.yellow
.frame(height: reader.safeAreaInsets.top, alignment: .top)
.ignoresSafeArea()
}
}
}
}
}
Use this UIApplication extension to chagne your status bar color
extension UIApplication {
/**
Get status bar view
*/
var statusBarUIView: UIView? {
let tag = 13101996
if let statusBar = self.windows.first?.viewWithTag(tag) {
self.windows.first?.bringSubviewToFront(statusBar)
return statusBar
} else {
let statusBarView = UIView(frame: UIApplication.shared.windows.first?.windowScene?.statusBarManager?.statusBarFrame ?? .zero)
statusBarView.tag = tag
self.windows.first?.addSubview(statusBarView)
return statusBarView
}
}
}
Usage
struct ContentViewStatusBar: View {
#State var name = ""
init() {
//Use this if NavigationBarTitle is with Large Font
UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.red]
UINavigationBar.appearance().backgroundColor = .gray
}
var body: some View {
NavigationView {
ZStack{
VStack{
TextField("Name", text: $name)
.frame(height:200)
.padding()
.background(backgrounImage())
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.gray,lineWidth: 4))
.padding()
Spacer()
}.navigationTitle("Tanvir")
.background(Color.green.edgesIgnoringSafeArea(.all))
}
}.onAppear {
UIApplication.shared.statusBarUIView?.backgroundColor = .gray //<<=== Here
}
}
}

Programatically scroll to SwiftUI list position? [duplicate]

It looks like in current tools/system, just released Xcode 11.4 / iOS 13.4, there will be no SwiftUI-native support for "scroll-to" feature in List. So even if they, Apple, will provide it in next major released, I will need backward support for iOS 13.x.
So how would I do it in most simple & light way?
scroll List to end
scroll List to top
and others
(I don't like wrapping full UITableView infrastructure into UIViewRepresentable/UIViewControllerRepresentable as was proposed earlier on SO).
SWIFTUI 2.0
Here is possible alternate solution in Xcode 12 / iOS 14 (SwiftUI 2.0) that can be used in same scenario when controls for scrolling is outside of scrolling area (because SwiftUI2 ScrollViewReader can be used only inside ScrollView)
Note: Row content design is out of consideration scope
Tested with Xcode 12b / iOS 14
class ScrollToModel: ObservableObject {
enum Action {
case end
case top
}
#Published var direction: Action? = nil
}
struct ContentView: View {
#StateObject var vm = ScrollToModel()
let items = (0..<200).map { $0 }
var body: some View {
VStack {
HStack {
Button(action: { vm.direction = .top }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { vm.direction = .end }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
ScrollViewReader { sp in
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
VStack(alignment: .leading) {
Text("Item \(item)").id(item)
Divider()
}.frame(maxWidth: .infinity).padding(.horizontal)
}
}.onReceive(vm.$direction) { action in
guard !items.isEmpty else { return }
withAnimation {
switch action {
case .top:
sp.scrollTo(items.first!, anchor: .top)
case .end:
sp.scrollTo(items.last!, anchor: .bottom)
default:
return
}
}
}
}
}
}
}
}
SWIFTUI 1.0+
Here is simplified variant of approach that works, looks appropriate, and takes a couple of screens code.
Tested with Xcode 11.2+ / iOS 13.2+ (also with Xcode 12b / iOS 14)
Demo of usage:
struct ContentView: View {
private let scrollingProxy = ListScrollingProxy() // proxy helper
var body: some View {
VStack {
HStack {
Button(action: { self.scrollingProxy.scrollTo(.top) }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { self.scrollingProxy.scrollTo(.end) }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
List {
ForEach(0 ..< 200) { i in
Text("Item \(i)")
.background(
ListScrollingHelper(proxy: self.scrollingProxy) // injection
)
}
}
}
}
}
Solution:
Light view representable being injected into List gives access to UIKit's view hierarchy. As List reuses rows there are no more values then fit rows into screen.
struct ListScrollingHelper: UIViewRepresentable {
let proxy: ListScrollingProxy // reference type
func makeUIView(context: Context) -> UIView {
return UIView() // managed by SwiftUI, no overloads
}
func updateUIView(_ uiView: UIView, context: Context) {
proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
}
}
Simple proxy that finds enclosing UIScrollView (needed to do once) and then redirects needed "scroll-to" actions to that stored scrollview
class ListScrollingProxy {
enum Action {
case end
case top
case point(point: CGPoint) // << bonus !!
}
private var scrollView: UIScrollView?
func catchScrollView(for view: UIView) {
if nil == scrollView {
scrollView = view.enclosingScrollView()
}
}
func scrollTo(_ action: Action) {
if let scroller = scrollView {
var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
switch action {
case .end:
rect.origin.y = scroller.contentSize.height +
scroller.contentInset.bottom + scroller.contentInset.top - 1
case .point(let point):
rect.origin.y = point.y
default: {
// default goes to top
}()
}
scroller.scrollRectToVisible(rect, animated: true)
}
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
Just scroll to the id:
scrollView.scrollTo(ROW-ID)
Since SwiftUI structured designed Data-Driven, You should know all of your items IDs. So you can scroll to any id with ScrollViewReader from iOS 14 and with Xcode 12
struct ContentView: View {
let items = (1...100)
var body: some View {
ScrollViewReader { scrollProxy in
ScrollView {
ForEach(items, id: \.self) { Text("\($0)"); Divider() }
}
HStack {
Button("First!") { withAnimation { scrollProxy.scrollTo(items.first!) } }
Button("Any!") { withAnimation { scrollProxy.scrollTo(50) } }
Button("Last!") { withAnimation { scrollProxy.scrollTo(items.last!) } }
}
}
}
}
Note that ScrollViewReader should support all scrollable content, but now it only supports ScrollView
Preview
Preferred way
This answer is getting more attention, but I should state that the ScrollViewReader is the right way to do this. The introspect way is only if the reader/proxy doesn't work for you, because of a version restrictions.
ScrollViewReader { proxy in
ScrollView(.vertical) {
TopView().id("TopConstant")
...
MiddleView().id("MiddleConstant")
...
Button("Go to top") {
proxy.scrollTo("TopConstant", anchor: .top)
}
.id("BottomConstant")
}
.onAppear{
proxy.scrollTo("MiddleConstant")
}
.onChange(of: viewModel.someProperty) { _ in
proxy.scrollTo("BottomConstant")
}
}
The strings should be defined in one place, outside of the body property.
Legacy answer
Here is a simple solution that works on iOS13&14:
Using Introspect.
My case was for initial scroll position.
ScrollView(.vertical, showsIndicators: false, content: {
...
})
.introspectScrollView(customize: { scrollView in
scrollView.scrollRectToVisible(CGRect(x: 0, y: offset, width: 100, height: 300), animated: false)
})
If needed the height may be calculated from the screen size or the element itself.
This solution is for Vertical scroll. For horizontal you should specify x and leave y as 0
Thanks Asperi, great tip. I needed to have a List scroll up when new entries where added outside the view. Reworked to suit macOS.
I took the state/proxy variable to an environmental object and used this outside the view to force the scroll. I found I had to update it twice, the 2nd time with a .5sec delay to get the best result. The first update prevents the view from scrolling back to the top as the row is added. The 2nd update scrolls to the last row. I'm a novice and this is my first stackoverflow post :o
Updated for MacOS:
struct ListScrollingHelper: NSViewRepresentable {
let proxy: ListScrollingProxy // reference type
func makeNSView(context: Context) -> NSView {
return NSView() // managed by SwiftUI, no overloads
}
func updateNSView(_ nsView: NSView, context: Context) {
proxy.catchScrollView(for: nsView) // here NSView is in view hierarchy
}
}
class ListScrollingProxy {
//updated for mac osx
enum Action {
case end
case top
case point(point: CGPoint) // << bonus !!
}
private var scrollView: NSScrollView?
func catchScrollView(for view: NSView) {
//if nil == scrollView { //unB - seems to lose original view when list is emptied
scrollView = view.enclosingScrollView()
//}
}
func scrollTo(_ action: Action) {
if let scroller = scrollView {
var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
switch action {
case .end:
rect.origin.y = scroller.contentView.frame.minY
if let documentHeight = scroller.documentView?.frame.height {
rect.origin.y = documentHeight - scroller.contentSize.height
}
case .point(let point):
rect.origin.y = point.y
default: {
// default goes to top
}()
}
//tried animations without success :(
scroller.contentView.scroll(to: NSPoint(x: rect.minX, y: rect.minY))
scroller.reflectScrolledClipView(scroller.contentView)
}
}
}
extension NSView {
func enclosingScrollView() -> NSScrollView? {
var next: NSView? = self
repeat {
next = next?.superview
if let scrollview = next as? NSScrollView {
return scrollview
}
} while next != nil
return nil
}
}
my two cents for deleting and repositioning list at any point based on other logic.. i.e. after delete/update, for example going to top.
(this is a ultra-reduced sample, I used this code after network call back to reposition: after network call I change previousIndex )
struct ContentView: View {
#State private var previousIndex : Int? = nil
#State private var items = Array(0...100)
func removeRows(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
self.previousIndex = offsets.first
}
var body: some View {
ScrollViewReader { (proxy: ScrollViewProxy) in
List{
ForEach(items, id: \.self) { Text("\($0)")
}.onDelete(perform: removeRows)
}.onChange(of: previousIndex) { (e: Equatable) in
proxy.scrollTo(previousIndex!-4, anchor: .top)
//proxy.scrollTo(0, anchor: .top) // will display 1st cell
}
}
}
}
This can now be simplified with all new ScrollViewProxy in Xcode 12, like so:
struct ContentView: View {
let itemCount: Int = 100
var body: some View {
ScrollViewReader { value in
VStack {
Button("Scroll to top") {
value.scrollTo(0)
}
Button("Scroll to buttom") {
value.scrollTo(itemCount-1)
}
ScrollView {
LazyVStack {
ForEach(0 ..< itemCount) { i in
Text("Item \(i)")
.frame(height: 50)
.id(i)
}
}
}
}
}
}
}
MacOS 11: In case you need to scroll a list based on input outside the view hierarchy. I have followed the original scroll proxy pattern using the new scrollViewReader:
struct ScrollingHelperInjection: NSViewRepresentable {
let proxy: ScrollViewProxy
let helper: ScrollingHelper
func makeNSView(context: Context) -> NSView {
return NSView()
}
func updateNSView(_ nsView: NSView, context: Context) {
helper.catchProxy(for: proxy)
}
}
final class ScrollingHelper {
//updated for mac os v11
private var proxy: ScrollViewProxy?
func catchProxy(for proxy: ScrollViewProxy) {
self.proxy = proxy
}
func scrollTo(_ point: Int) {
if let scroller = proxy {
withAnimation() {
scroller.scrollTo(point)
}
} else {
//problem
}
}
}
Environmental object:
#Published var scrollingHelper = ScrollingHelper()
In the view: ScrollViewReader { reader in .....
Injection in the view:
.background(ScrollingHelperInjection(proxy: reader, helper: scrollingHelper)
Usage outside the view hierarchy: scrollingHelper.scrollTo(3)
As mentioned in #lachezar-todorov's answer Introspect is a nice library to access UIKit elements in SwiftUI. But be aware that the block you use for accessing UIKit elements are being called multiple times. This can really mess up your app state. In my cas CPU usage was going %100 and app was getting unresponsive. I had to use some pre conditions to avoid it.
ScrollView() {
...
}.introspectScrollView { scrollView in
if aPreCondition {
//Your scrolling logic
}
}
Another cool way is to just use namespace wrappers:
A dynamic property type that allows access to a namespace defined by the persistent identity of the object containing the property (e.g. a view).
struct ContentView: View {
#Namespace private var topID
#Namespace private var bottomID
let items = (0..<100).map { $0 }
var body: some View {
ScrollView {
ScrollViewReader { proxy in
Section {
LazyVStack {
ForEach(items.indices, id: \.self) { index in
Text("Item \(items[index])")
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.green.cornerRadius(16))
}
}
} header: {
HStack {
Text("header")
Spacer()
Button(action: {
withAnimation {
proxy.scrollTo(bottomID)
}
}
) {
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
.padding(.vertical)
.id(topID)
} footer: {
HStack {
Text("Footer")
Spacer()
Button(action: {
withAnimation {
proxy.scrollTo(topID) }
}
) {
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
}
.padding(.vertical)
.id(bottomID)
}
.padding()
}
}
.foregroundColor(.white)
.background(.black)
}
}
Two parts:
Wrap the List (or ScrollView) with ScrollViewReader
Use the scrollViewProxy (that comes from ScrollViewReader) to scroll to an id of an element in the List. You can seemingly use EmptyView().
The example below uses a notification for simplicity (use a function if you can instead!).
ScrollViewReader { scrollViewProxy in
List {
EmptyView().id("top")
}
.onReceive(NotificationCenter.default.publisher(for: .ScrollToTop)) { _ in
// when using an anchor of `.top`, it failed to go all the way to the top
// so here we add an extra -50 so it goes to the top
scrollViewProxy.scrollTo("top", anchor: UnitPoint(x: 0, y: -50))
}
}
extension Notification.Name {
static let ScrollToTop = Notification.Name("ScrollToTop")
}
NotificationCenter.default.post(name: .ScrollToTop, object: nil)