I'd like to add a full size AccessoryWidgetBackground() to an accessoryRectangular widget family.
I made a brand new project and added a brand new widget. Then I changed the view to:
struct Some_WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
AccessoryWidgetBackground()
.frame(width: .infinity, height: .infinity)
.ignoresSafeArea()
}
}
Here's what I get:
Is there a way to take up the whole view with the background blur?
Here is a possible solution. Tested with Xcode 11.4 / iOS 13.4
struct Some_WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
ZStack {
Color.clear.edgesIgnoringSafeArea(.all) // << here !!
AccessoryWidgetBackground()
.edgesIgnoringSafeArea(.all)
}
}
}
The answer to the question as posed is: this is not possible in iOS 16.0, the view is cropped as can be seen from the inner box in the image in the question.
The workaround I settled for is to create a rounded background view and add it in a ZStack:
var body: some View {
ZStack {
OptionalBlurView(showBlur: true)
// etc
Here's the background view, it works in previews too.
#available(iOSApplicationExtension 16.0, *)
struct OptionalBlurView: View {
var showBlur: Bool
#Environment(\.widgetFamily) var family
var body: some View {
if showBlur {
blurView
} else {
EmptyView()
}
}
var blurView: some View {
#if targetEnvironment(simulator)
// at the time of coding, AccessoryWidgetBackground does not show in previews, so this is an aproximation
switch family {
case .accessoryCircular:
return Circle()
.opacity(0.3)
.eraseToAnyView()
default:
return Rectangle()
.clipShape(RoundedRectangle(cornerSize: CGSize(width: 10, height: 10), style: .continuous))
.opacity(0.3)
.eraseToAnyView()
}
#else
AccessoryWidgetBackground()
.clipShape(RoundedRectangle(cornerSize: CGSize(width: 10, height: 10), style: .continuous))
.opacity(0.7)
#endif
}
}
Related
I'm trying to remove the "row" separators (known as dividers in SwiftUI) from a List in SwiftUI.
I went through the List documentation, but I haven't been able to find a modifier for that.
Any help would be appreciated.
iOS 15:
This year Apple introduced a new modifier .listRowSeparator that can be used to style the separators. you can pass .hidden to hide it:
List {
ForEach(items, id:\.self) {
Text("Row \($0)")
.listRowSeparator(.hidden)
}
}
iOS 14
Apple introduced LazyVStack In iOS 14. you may consider using it instead of list for this:
ScrollView {
LazyVStack {
ForEach((1...100), id: \.self) {
Text("Placeholder \($0)")
}
}
}
Keep in mind that LazyVStack is lazy and doesn't render all rows all the time. So they are very performant and suggested by Apple itself in WWDC 2020.
iOS 13
There is a UITableView behind SwiftUI's List for iOS. So to remove
Extra separators (below the list):
you need a tableFooterView and to remove
All separators (including the actual ones):
you need separatorStyle to be .none
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
}
var body: some View {
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
}
iOS 13 builds only
while this solution works correctly let's clean up the work using
ViewModifier
public struct ListSeparatorStyleNoneModifier: ViewModifier {
public func body(content: Content) -> some View {
content.onAppear {
UITableView.appearance().separatorStyle = .none
}.onDisappear {
UITableView.appearance().separatorStyle = .singleLine
}
}
}
now let's make a small extension that would help to hide the details
extension View {
public func listSeparatorStyleNone() -> some View {
modifier(ListSeparatorStyleNoneModifier())
}
}
As you can see, we’ve wrapped our appearance setting code into a neat little view modifier.
you can declare it directly now
List {
Text("1")
Text("2")
Text("3")
}.listSeparatorStyleNone()
You may use ForEach within a ScrollView instead of List for dynamic views without any styling
iOS 13 builds only:
The current workaround is to remove them via UIAppearance:
UITableView.appearance(whenContainedInInstancesOf:
[UIHostingController<ContentView>.self]
).separatorStyle = .none
iOS 13 builds only:
Adding UITableView.appearance().separatorColor = .clear for initializer
struct SomeView: View {
init() {
UITableView.appearance().separatorColor = .clear
}
}
I hope you to resolve this problem.
iOS 13 builds only:
See existing answers using UITableView.appearance().
⚠️ Be aware that in the iOS 14 SDK, List does not appear to be backed by UITableView. See the alternate solution below:
iOS 14 Xcode 12 Beta 1 only:
I do have a pure SwiftUI solution for iOS 14, but who knows how long it's going to continue working for. It relies on your content being the same size (or larger) than the default list row and having an opaque background.
⚠️ This does not work for iOS 13 builds.
Tested in Xcode 12 beta 1:
yourRowContent
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
.frame(
minWidth: 0, maxWidth: .infinity,
minHeight: 44,
alignment: .leading
)
.listRowInsets(EdgeInsets())
.background(Color.white)
Or if you're looking for a reusable ViewModifier:
import SwiftUI
struct HideRowSeparatorModifier: ViewModifier {
static let defaultListRowHeight: CGFloat = 44
var insets: EdgeInsets
var background: Color
init(insets: EdgeInsets, background: Color) {
self.insets = insets
var alpha: CGFloat = 0
UIColor(background).getWhite(nil, alpha: &alpha)
assert(alpha == 1, "Setting background to a non-opaque color will result in separators remaining visible.")
self.background = background
}
func body(content: Content) -> some View {
content
.padding(insets)
.frame(
minWidth: 0, maxWidth: .infinity,
minHeight: Self.defaultListRowHeight,
alignment: .leading
)
.listRowInsets(EdgeInsets())
.background(background)
}
}
extension EdgeInsets {
static let defaultListRowInsets = Self(top: 0, leading: 16, bottom: 0, trailing: 16)
}
extension View {
func hideRowSeparator(
insets: EdgeInsets = .defaultListRowInsets,
background: Color = .white
) -> some View {
modifier(HideRowSeparatorModifier(
insets: insets,
background: background
))
}
}
struct HideRowSeparator_Previews: PreviewProvider {
static var previews: some View {
List {
ForEach(0..<10) { _ in
Text("Text")
.hideRowSeparator()
}
}
.previewLayout(.sizeThatFits)
}
}
From: Swiftui Views Mastery Book SwiftUI 2.0 Mark Moeykens
.listStyle(SidebarListStyle()) # IOS 14
You can apply this new list style which will remove the separator lines.
For iOS13,iOS14,iOS15,and remove the separator at the top of the first cell
Add viewModifier
extension View {
/// 隐藏 List 中的 分割线
func hideRowSeparator(insets: EdgeInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0),
background: Color = .white) -> some View {
modifier(HideRowSeparatorModifier(insets: insets, background: background))
}
}
struct HideRowSeparatorModifier: ViewModifier {
static let defaultListRowHeight: CGFloat = 44
var insets: EdgeInsets
var background: Color
init(insets: EdgeInsets, background: Color) {
self.insets = insets
var alpha: CGFloat = 0
if #available(iOS 14.0, *) {
UIColor(background).getWhite(nil, alpha: &alpha)
assert(alpha == 1, "Setting background to a non-opaque color will result in separators remaining visible.")
}
self.background = background
}
func body(content: Content) -> some View {
content
.padding(insets)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: Self.defaultListRowHeight)
.listRowInsets(EdgeInsets())
.overlay(
VStack {
HStack {}
.frame(maxWidth: .infinity)
.frame(height: 1)
.background(background)
Spacer()
HStack {}
.frame(maxWidth: .infinity)
.frame(height: 1)
.background(background)
}
.padding(.top, -1)
)
}
}
Usage
struct ContentView: View {
var body: some View {
List {
ForEach(0 ..< 30) { item in
HStack(alignment: .center, spacing: 30) {
Text("Hello, world!:\(item)").padding()
}
.hideRowSeparator(background: .white)
}
}
.listStyle(PlainListStyle())
}
}
You can find here
https://github.com/wangrui460/HiddenListLine4SwiftUI
iOS / SwiftUI 技术交流
我创建了一个 微信 iOS 技术交流群、SwiftUI 技术交流群,欢迎小伙伴们加入一起交流学习~
可以加我微信我拉你进去(备注iOS),我的微信号 wr1204607318
I'm building a macOS 13 app using SwiftUI. The app has two-column navigation by using the NavigationSplitView which gives a sidebar and detail view. The detail views are different sizes so I would like the window to change size based on the size of each detail view.
In the example below, the detail views are AppleView, KiwiView, and PeachView. The AppleView has a size of 400x300 and the KiwiView is 300x200. When I run the app, the window does not adjust its size when the detail view changes. I tried to wrap the navigation view in a VStack but that did not help. Does anyone know how I can get the app's window to adjust size based on the selected detail view?
import SwiftUI
struct AppleView: View {
var body: some View {
Text("Apple View 🍎")
.font(.title)
.frame(width: 400, height: 300)
.background(.red)
}
}
struct KiwiView: View {
var body: some View {
Text("Kiwi View 🥝")
.font(.title)
.frame(width: 300, height: 200)
.background(.green)
}
}
struct PeachView: View {
var body: some View {
Text("Peach View 🍑")
.font(.title)
.background(.pink)
}
}
enum Fruit: String, CaseIterable {
case apple = "Apple"
case kiwi = "Kiwi"
case peach = "Peach"
}
struct ContentView: View {
#State private var selectedFruit: Fruit = .apple
var body: some View {
NavigationSplitView {
List(Fruit.allCases, id: \.self, selection: $selectedFruit) { fruit in
Text(fruit.rawValue)
}
} detail: {
switch selectedFruit {
case .apple:
AppleView()
case .kiwi:
KiwiView()
case .peach:
PeachView()
}
}
}
}
I also tried setting the .windowResizability() of the main window group but that didn't fix the problem.
import SwiftUI
#main
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowResizability(.contentSize)
}
}
sorry for the late reply..
It would apply to NavigationSplitView just the same.
Since you want to have fixed sizes, I would add frames to all the sub-Views for consistent behaviour, so in this example the PeachView and the List
and then you simply modify your ContentView accordingly:
struct ContentView: View {
#State private var selectedFruit: Fruit = .apple
#State private var window: NSWindow?
var body: some View {
NavigationSplitView {
List(Fruit.allCases, id: \.self, selection: $selectedFruit) { fruit in
Text(fruit.rawValue)
}
.frame(width: 100)
} detail: {
switch selectedFruit {
case .apple:
AppleView()
case .kiwi:
KiwiView()
case .peach:
PeachView()
}
}
.background(WindowAccessor(window: $window))
.onChange(of: selectedFruit) { newValue in
if newValue == .apple {
window?.setFrame(NSRect(x: 600, y: 400, width: 500, height: 600), display: true)
} else if newValue == .kiwi {
window?.setFrame(NSRect(x: 600, y: 400, width: 400, height: 500), display: true)
} else if newValue == .peach {
window?.setFrame(NSRect(x: 600, y: 400, width: 350, height: 400), display: true)
}
}
}
}
And then add the WindowAccessor :
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
For full access of the Window across the app you might also wanna look into accessing it with App/Scene Delegate, as explained here
EDIT:
Addressing shifting behaviour
You can position the Window anywhere you want, specified by the x and y coordinates of the NSRect you create in window?.setFrame. Since you didnt say what kind of positioning you'd like, I now 'pinned' the window to the top-left-corner, by accessing the underlying screen as follows:
.onChange(of: selectedFruit) { newValue in
if newValue == .apple {
window?.setFrame(NSRect(x: 0, y: window?.screen?.frame.maxY ?? 500, width: 500, height: 600), display: true)
} else if newValue == .kiwi {
window?.setFrame(NSRect(x: 0, y: window?.screen?.frame.maxY ?? 500, width: 400, height: 500), display: true)
} else if newValue == .peach {
window?.setFrame(NSRect(x: 0, y: window?.screen?.frame.maxY ?? 500, width: 400, height: 300), display: true)
}
}
For more complex positions you just need to calculate frame dimensions and origin, according to your wanted behaviour. The detail view beeing clipped is fixed by adjusting frame size.
Further down the line you might find this helpful when dealing with MacOS saving the last position and size of the window.
In my app I have a NavigationStack inside the detail of a NavigationSplitView. With the code below the navigation back and forward works fine but if from the detail, I incompletely swipe left to dismiss the view, the NavigationLink doesn't work anymore. This code is partially copied from Apple Documentation.
Does anyone know what causes the problem?
struct ContentView: View {
let colors: [Color] = [.purple, .pink, .orange]
#State private var selection: Color? = nil
var body: some View {
NavigationSplitView {
List(colors, id: \.self, selection: $selection) { color in
NavigationLink(color.description, value: color)
}
} detail: {
NavigationStack {
if let color = selection {
VStack {
NavigationLink(color.description, value: color)
}
.navigationDestination(for: Color.self) { color in
RoundedRectangle(cornerRadius: 10)
.fill(color)
.frame(width: 150, height: 150)
}
} else {
Text("Pick a color")
}
}
}
}
}
EDIT:
I've made some code improvements creating separate struct for each view to avoid confusion and better understand the code but the problem persist.
struct ContentView: View {
let colors: [Color] = [.purple, .pink, .orange]
#State private var selection: Color? = nil
var body: some View {
NavigationSplitView {
List(colors, id: \.self, selection: $selection) { color in
NavigationLink(color.description, value: color)
}
} detail: {
NavigationStack {
if let color = selection {
DetailView(color: color)
} else {
Text("Pick a color")
}
}
}
}
}
struct DetailView: View {
let color: Color
var body: some View {
VStack {
NavigationLink("Go to third view", value: color)
.padding()
.background(color)
.foregroundColor(.white)
}
.navigationTitle("Detail View")
.navigationDestination(for: Color.self) { color in
ThirdView(color: color)
}
}
}
struct ThirdView: View {
let color: Color
var body: some View {
RoundedRectangle(cornerRadius: 10)
.fill(color)
.frame(width: 150, height: 150)
.navigationTitle("Third View")
}
}
It looks like the bug is fixed. At least it works as expected on the latest Xcode 14.1 RC.
I created a custom LoadingView as a Indicator for loading objects from internet. When add it to NavigationView, it shows like this
enter image description here
I only want it showing in the middle of screen rather than move from top left corner
Here is my Code
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.primaryDota, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
and my content view
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
.frame(width: 30, height: 30)
}
}
}
This looks like a bug of NavigationView: without it animation works totally fine. And it wan't fixed in iOS15.
Working solution is waiting one layout cycle using DispatchQueue.main.async before string animation:
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.red, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
}
This is a bug from NavigationView, I tried to kill all possible animation but NavigationView ignored all my try, NavigationView add an internal animation to children! here all we can do right now!
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
}
}
}
struct LoadingView: View {
#State private var isLoading: Bool = Bool()
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.blue, lineWidth: 5.0)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
.onAppear { DispatchQueue.main.async { isLoading.toggle() } }
}
}
I have a problem regarding behavior of an animated loading view. The loading view shows up while the network call. I have an isLoading #Published var inside my viewModel and the ActivityIndicator is shown inside a ZStack in my view. The Activityindicator is a custom view where I animate a trimmed circle - rotate it. Whenever the activity indicator is shown inside my mainView it has a weird transition when appearing- it is transitioned from the top left corner to the center of the view. Does anyone know why is this happening? I attach the structs with the code and 3 pictures with the behavior.
ActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
CircleView(animate: $animate, firstGradientColor: orangeColor, secondGradientColor: orangeColorOpaque, style: style)
}.onAppear() {
self.animate.toggle()
}
}
}
struct CircleView: View {
#Binding var animate: Bool
var firstGradientColor: Color
var secondGradientColor: Color
var style: StrokeStyle
var body: some View {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [firstGradientColor, secondGradientColor]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.transition(.opacity)
.animation(Animation.linear(duration: 0.7) .repeatForever(autoreverses: false), value: animate)
}
}
The view is use it in :
struct UserProfileView: View {
#ObservedObject var viewModel: UserProfileViewModel
#Binding var lightMode: ColorScheme
var body: some View {
NavigationView {
ZStack {
VStack(alignment: .center, spacing: 12) {
HStack {
Text(userProfileEmail)
.font(.headline)
.foregroundColor(Color(UIColor.label))
Spacer()
}.padding(.bottom, 16)
SettingsView(userProfile: $viewModel.userProfile, isDarkMode: $viewModel.isDarkMode, lightMode: $lightMode, location: viewModel.locationManager.address, viewModel: viewModel)
ButtonsView( userProfile: $viewModel.userProfile)
Spacer()
}.padding([.leading, .trailing], 12)
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
// .animation(nil)
}
}
}
}
}
I also tried with animation nil but it doesn't seem to work.
Here are the pictures:
Here is a possible solution - put it over NavigationView. Tested with Xcode 12.4 / iOS 14.4
NavigationView {
// .. your content here
}
.overlay( // << here !!
VStack {
if isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
)