Unable to use custom alignment with non-sibling views with animation - swiftui

Goal: to have the tapped person in the ScrollView/HStack align it's centre with the other views using the explicit custom alignment, CentreScreenAlignment
Problem: currently nothing is happening when I tap on a given person.
Code:
extension HorizontalAlignment {
private enum CentreScreenAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let centreScreenAlignment = HorizontalAlignment(CentreScreenAlignment.self)
}
struct ContentView: View {
#State private var centrePt: Int = 0
var body: some View {
GeometryReader { g in
VStack(alignment: .centreScreenAlignment) {
ZStack {
HStack {
Button("Cancel") {
// action
}
.padding(.leading)
Spacer()
Button("Done") {
// action
}
.padding(.trailing)
}
Button {
// action
} label: {
Image(systemName: "person.fill.badge.plus")
.coordinateSpace(name: "Add button")
.foregroundColor(.blue)
.alignmentGuide(.centreScreenAlignment) { d in
d[.centreScreenAlignment]
}
}
}
EmptyView()
.alignmentGuide(.centreScreenAlignment) { d in
d[.centreScreenAlignment]
}
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(1..<10) { index in
Group {
if index == self.centrePt {
Button("Patient #\(index)") {
//
}.transition(AnyTransition.identity)
.alignmentGuide(.centreScreenAlignment) { d in
d[.centreScreenAlignment]
}
} else {
Button("Person #\(index)") {
withAnimation {
centrePt = index
print(centrePt)
}
}
.transition(AnyTransition.identity)
}
}
}
}
}.padding()
Text("Hello")
}
}
}
}
Thanks very much.

Related

How should I code this `#ViewBuilder` function for conditional contextMenu?

Because the code for compositing context menu is a little lengthy, I want to code it like this:
var body: some View {
Text("Hello World")
.contextMenu(changingStatusID == nil ? contextMenu() : nil)
}
#ViewBuilder
func contextMenu() -> ContextMenu { //<-- Problem goes here, I don't know how to set the parameters and it's return type
ContextMenu {
Button {
edit()
} label: {
Label("Edit", systemImage: "rectangle.and.pencil.and.ellipsis")
}
Divider()
if #available(macOS 12.0, *) {
Button(role: .destructive) {
do {
try delete()
}
catch {
showAlert = true
}
} label: {
Label("Delete", systemImage: "trash")
}
.alert("Failed to delete.", isPresented: $showAlert) {
Button("OK", role: .cancel) { }
}
} else {
Button {
do {
try delete()
}
catch {
showAlert = true
}
} label: {
Label("Delete ⚠︎", systemImage: "trash")
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Failed"),
message: Text("Unable to delete this task."),
dismissButton: .default(Text("OK")))
}
}
}
}
XCode shows error in the line of function name func contextMenu():
Cannot convert value of type 'ContextMenu<TupleView<(Button<Label<Text, Image>>, Divider, _ConditionalContent<AnyView, some View>)>>' to specified type '<<error type>>'
Reference to generic type 'ContextMenu' requires arguments in <...> Insert '<<#MenuItems: View#>>'
Static method 'buildBlock' requires that 'ContextMenu<TupleView<(Button<Label<Text, Image>>, Divider, _ConditionalContent<AnyView, some View>)>>' conform to 'View'
Here is an approach that works:
struct ContentView: View {
#State private var showContext = true
#State private var showAlert = false
var body: some View {
VStack {
Toggle("show contextMenu", isOn: $showContext)
Text("Hello World")
.contextMenu {
if showContext {
myContextMenu()
}
}
}
.padding()
}
#ViewBuilder
func myContextMenu() -> some View {
// ContextMenu { // not needed
Button {
//edit()
} label: {
Label("Edit", systemImage: "rectangle.and.pencil.and.ellipsis")
}
Divider()
Button(role: .destructive) {
do {
//try delete()
}
catch {
showAlert = true
}
} label: {
Label("Delete", systemImage: "trash")
}
.alert("Failed to delete.", isPresented: $showAlert) {
Button("OK", role: .cancel) { }
}
// }
}
}

`.transition(.move(edge: .bottom))` for element inside `ZStack` not working with `withAnimation` on button

I'm trying to build a simple animated overlay. Ideally, the dark background fades in (which it's doing now) and the white card slides up from the bottom edge (using .transition(.move(edge: .bottom).
Here's my ContentView.swift file:
struct Overlays: View {
#State var showOverlay = false
var body: some View {
NavigationView {
Button {
withAnimation(.spring()) {
showOverlay.toggle()
}
} label: {
Text("Open overlay")
}
.navigationTitle("Overlay demo")
}
.overlay {
if showOverlay {
CustomOverlay(
overlayPresented: $showOverlay,
overlayContent: "This is a real basic overlay, and it should be sliding in from the bottom."
)
}
}
}
}
And here's my CustomOverlay.swift file:
struct CustomOverlay: View {
#Binding var overlayPresented: Bool
let overlayContent: String
var body: some View {
ZStack(alignment: .bottom) {
overlayBackground
overlayCard
}
}
}
extension CustomOverlay {
var overlayBackground: some View {
Color.black.opacity(0.6)
.ignoresSafeArea(.all)
.onTapGesture {
withAnimation(.spring()) {
overlayPresented = false
}
}
}
var overlayCard: some View {
VStack(spacing: 16) {
overlayText
overlayCloseButton
}
.padding()
.frame(maxWidth: .infinity)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.padding()
.transition(.move(edge: .bottom))
}
var overlayText: some View {
Text(overlayContent)
}
var overlayCloseButton: some View {
Button {
withAnimation(.spring()) {
overlayPresented = false
}
} label: {
Text("Close")
}
}
}
This doesn't appear to work. The entire overlay is fading in/out.
https://imgur.com/a/iRzJCsw
If I move the .transition(.move(edge: .bottom) to the CustomOverlay ZStack the entire overlay slides in from the bottom which looks super goofy.
What am I doing wrong?
After some more experimentation, I've found something pretty cool.
Our main ContentView.swift file:
struct Overlays: View {
#State var showOverlay = false
var body: some View {
NavigationView {
Button {
withAnimation(.easeInOut(duration: 0.25)) {
showOverlay.toggle()
}
} label: {
Text("Open overlay")
}
.navigationTitle("Overlay demo")
}
.overlay {
if showOverlay {
// Here's the overlay background, which we can animate independently
OverlayBackground(
overlayPresented: $showOverlay
)
.transition(.opacity)
// Explicit z-index as per https://stackoverflow.com/a/58512696/1912818
.zIndex(0)
// Here's the overlay content card, which we can animate independently too!
OverlayContent(
overlayPresented: $showOverlay,
overlayContent: "This is a real basic overlay, and it should be sliding in from the bottom."
)
.transition(.move(edge: .bottom).combined(with: .opacity))
// Explicit z-index as per https://stackoverflow.com/a/58512696/1912818
.zIndex(1)
}
}
}
}
And here's OverlayBackground.swift (the background):
struct OverlayBackground: View {
#Binding var overlayPresented: Bool
var body: some View {
Color.black.opacity(0.6)
.ignoresSafeArea(.all)
.onTapGesture {
withAnimation(.easeInOut(duration: 0.25)) {
overlayPresented = false
}
}
}
}
And lastly OverlayContent.swift:
struct OverlayContent: View {
#Binding var overlayPresented: Bool
let overlayContent: String
var body: some View {
VStack {
Spacer()
overlayCard
}
}
}
extension OverlayContent {
var overlayCard: some View {
VStack(spacing: 16) {
overlayText
overlayCloseButton
}
.padding()
.frame(maxWidth: .infinity)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.padding()
}
var overlayText: some View {
Text(overlayContent)
}
var overlayCloseButton: some View {
Button {
withAnimation(.easeInOut(duration: 0.25)) {
overlayPresented = false
}
} label: {
Text("Close")
}
}
}
The result: https://imgur.com/a/1JoMWcs

ForEach Loop in Swift for Buttons

I want to use a ForEach loop to simplify the following code:
.toolbar {
ToolbarItem() {
Button {
}
label: {
Image(systemName: "magnifyingglass")
}
}
ToolbarItem() {
Button {
}
label: {
Image(systemName: "plus")
}
}
}
But it's not working. My approach only creates the "magnifyingglass" button.
My approach:
let toolbar = ["magnifyingglass", "plus"]
.toolbar {
ToolbarItem() {
ForEach(toolbar.indices) { index in
Button {
}
label: {
Image(systemName: toolbar[index])
}
}
}
}
you could try this:
struct ContentView: View {
let toolbar = ["magnifyingglass", "plus"]
var body: some View {
NavigationView {
Text("testing")
.toolbar {
ToolbarItem() {
HStack { // <--- here
ForEach(toolbar.indices) { index in
Button { }
label: { Image(systemName: toolbar[index]) }
}
}
}
}
}
}
}
import SwiftUI
struct ContentView: View {
let toolbar = ["magnifyingglass", "plus"]
var body: some View {
NavigationView {
Text("Toolbar")
.navigationTitle("")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
ForEach(toolbar, id: \.self) { index in
Button {
} label: {
Image(systemName: ("\(index)"))
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I suggest (not tested) to put the for loop outside of ToolbarItem() :
let toolbar = ["magnifyingglass", "plus"]
.toolbar {
ForEach(toolbar.indices) { index in
ToolbarItem() {
Button {
}
label: {
Image(systemName: toolbar[index])
}
}
}
}

TabView disconnects when rotating to Landscape due to SwiftUI's re-render of parent-Views

Using Swift5.3.2, iOS14.4.1, XCode12.4,
As the following code shows, I am working with a quite complex TabView in Page-Mode in SwiftUI.
i.e. using iOS14's new possibility to show Pages:
.tabViewStyle(PageTabViewStyle())
Everything works.
Except, if I rotate my iPhone from Portrait to Landscape, the TabView disconnects and sets the selectedTab index to 0 (i.e. no matter where you scrolled to, rotating iPhone resets unwontedly to page 0).
The parent-View itself is in a complex View hierarchy. And one of the parent-View's of the TabView is updated during the TabView is shown (and swiped). And this might be the problem that the TabView gets re-rendered when rotating to Landscape.
What can I do to keep the TabView-Page during iPhone rotation ??
Here is the code:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
MediaTabView()
CloseButtonView()
}
}
}
And the MediaTabView at question:
struct MediaTabView: View {
#EnvironmentObject var appStateService: AppStateService
#EnvironmentObject var commService: CommunicationService
#State private var tagID = ""
#State private var selectedTab = 0
#State private var uniqueSelected = 0
#State private var IamInSwipingAction = false
var body: some View {
let myDragGesture = DragGesture(minimumDistance: 10)
.onChanged { _ in
IamInSwipingAction = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5000)) {
IamInSwipingAction = false // workaround: because onEnded does not work...
}
}
.onEnded { _ in
IamInSwipingAction = false
}
TabView(selection: self.$selectedTab) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
switch appStateService.appState {
case .content(let tagID):
return list.tagId == tagID
default:
return false
}
}) {
if list.paths.count > 0 {
ForEach(list.paths.indices, id: \.self) { index in
ZoomableScrollView {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
} else if url.containsVideo {
CustomPlayerView(url: url)
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.onAppear() {
if uniqueSelected != selectedTab {
uniqueSelected = selectedTab
if IamInSwipingAction && (commService.communicationRole == .moderatorMode) {
commService.send(thCmd: THCmd(key: .swipeID, sender: "", content: URL(fileURLWithPath: list.paths[index]).lastPathComponent))
}
}
}
}
} else {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
if Constants.TrihowAlbum.tagIdArrayTrihowAlbum.contains(tagID) {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
} else {
Text(LocalizedStringKey("TagNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
}
.onAppear() {
switch appStateService.appState {
case .content(let tagID):
self.tagID = tagID
default:
self.tagID = ""
}
}
.tabViewStyle(PageTabViewStyle())
.onTHComm_ReceiveCmd(service: commService) { (thCmd) in
switch thCmd.key {
case .swipeID:
if (commService.communicationRole == .moderatorMode) || (commService.communicationRole == .discoveryMode) {
selectTabFromCmdID(fileName: thCmd.content)
} else {
break
}
default:
break
}
}
.simultaneousGesture(myDragGesture)
}
}
extension MediaTabView {
private func selectTabFromCmdID(fileName: String) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
return list.tagId == tagID
}) {
if list.paths.count > 0 {
if let idx = list.paths.firstIndex(where: { (urlPath) -> Bool in
if let url = URL(string: urlPath) {
return url.lastPathComponent == fileName
} else { return false }
}) {
selectedTab = idx
}
}
}
}
}

SwiftUI - centre content on a List

How can I make this arrow on the centre of the list?
struct ProductsList : View {
var body: some View {
VStack {
List {
Image(systemName: "shift")
}
}
}
}
You may just wanna use some Spacers.
struct ProductList : View {
var body: some View {
VStack {
List {
HStack {
Spacer()
Image(systemName: "shift")
Spacer()
}
}
}
}
}
I would suggest to use ViewModifier:
struct CenterModifier: ViewModifier {
func body(content: Content) -> some View {
HStack {
Spacer()
content
Spacer()
}
}
}
so that in your list, if you have more different type of UI elements its more convenient way to do so:
struct ExampleList: View {
var body: some View {
List {
Image(systemName: "shift").modifier(CenterModifier())
SomeOtherView().modifier(CenterModifier())
}
}
}
Try this:
var body: some View {
List {
GeometryReader { geometry in
VStack(alignment: .center) {
Image(systemName: "shift")
}.frame(width: geometry.size.width)
}
}
}