I'm supporting iOS 13 and up. I wish to use the new .toolbar modifier for an optional feature, in devices using iOS 14 and up. I'm using Xcode 13.0 RC 1, build 13A233, but the issue also occurs in Xcode 12.5.1. I have created the following code:
import SwiftUI
struct MyToolbar<T>: ViewModifier where T: View {
let toolbarContent: () -> T
func body(content: Content) -> some View {
if #available(iOS 14.0, *) {
content
.toolbar(content: self.toolbarContent)
}
}
}
extension View {
func myToolbar<T: View>(#ViewBuilder content: #escaping () -> T) -> some View {
self.modifier(MyToolbar(toolbarContent: content))
}
}
struct MyToolbar_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
Text("Hello, world")
.myToolbar {
if #available(iOS 14.0, *) {
Text("Hello, world!")
// ToolbarItem(placement: .bottomBar) {
// Text("Hello, world!")
// }
} else {
EmptyView()
}
}
}
}
}
I have included the preview bit, so the above can be pasted into an empty file in any Xcode SwiftUI project. It works. When I uncomment the ToolbarItem statement, I get the following errors:
Static method 'buildBlock' requires that 'ToolbarItem<Void, Text>' conform to 'View'
Static method 'buildLimitedAvailability' requires that 'ToolbarItem<Void, Text>' conform to 'View'
How can I fix these errors?
I think the main issue is that the .toolbar modifier's block is defined to return something conforming to the ToolbarContent protocol. From the docs:
func toolbar<Content>(content: () -> Content) -> some View where Content : ToolbarContent
As such, within your view modifier
func body(content: Content) -> some View {
if #available(iOS 14.0, *) {
content
.toolbar(content: self.toolbarContent)
}
}
… you need to ensure that self.toolbarContent matches that () -> ToolbarContent type signature.
Just from glancing at your code, I'd be inclined to first try changing your conformance of T from T: View to T: ToolbarContent. By eye that looks like it might be enough, but it's possible you might need to do a bit more work to tidy everything up.
Related
I'm assuming I should probably file this as a feedback report with Apple, but posting here in case I am missing something - or if there is new guidance with latest SwiftUI.
This code works as expected in Xcode 13, but in Xcode 14 beta 2, the navigation bar and "Cancel" button are missing. Is this ProgressView with deferred content loading somehow a technique that doesn't work anymore?
import SwiftUI
struct ContentView: View {
#State private var isFlowDetermined = false
var body: some View {
NavigationView {
//NestedView()
if self.isFlowDetermined {
NestedView()
} else {
ProgressView()
.task {
await self.determineFlow()
}
}
}
}
private func determineFlow() async {
self.isFlowDetermined = true
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct NestedView: View {
var body: some View {
ScrollView {
Text("Where is the \"Cancel\" button?")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
#if !os(macOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
#if !os(macOS)
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
print("got here")
}
}
#endif
}
}
}
UPDATE: Xcode 14 beta 4 appears to resolve this issue. The suggested workaround below is no longer needed.
It seems they optimised toolbar construction (and don't assume it is changed). Anyway I see only one workaround for now:
NavigationView {
// .. content
}
.id(isFlowDetermined) // << here !!
Tested with Xcode 14b2 / iOS 16
*Note: NavigationView is deprecated since iOS 16
I want to make a list view which contents could be dragged for reordering and tapped for detail.
But when I add the onTapGesture() action in the content view, the onMove() action stop working.
Example code:
import SwiftUI
struct TestTapAndMove: View {
#State private var users = ["Paul", "Taylor", "Adele"]
var body: some View {
List {
ForEach(users, id: \.self) { user in
VStack {
Text("user: \(user)").font(.headline)
Divider()
}.background(Color.gray)
// The TapGesture will result in onMove action not working.
.onTapGesture(perform: {
print("tap!")
})
}
.onMove(perform: move)
}
}
func move(from source: IndexSet, to destination: Int) {
print("onMove!")
users.move(fromOffsets: source, toOffset: destination)
}
}
Is there any solution that could make the tap and move action work together?
As workaround you can make some specific part of row clickable, like text in below example (or some added custom shape or plain button)
VStack {
Text("user: \(user)").font(.headline)
.onTapGesture(perform: {
print("tap!")
})
Divider()
}.background(Color.gray)
Alternate: you can add helper custom view as row overlay which would handle tap/mouseDown action for view (and does not break drug)
class TapHandlerView: NSView {
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event) // keep this to allow drug !!
print("tap")
}
}
struct TapHandler: NSViewRepresentable {
func makeNSView(context: Context) -> TapHandlerView {
TapHandlerView()
}
func updateNSView(_ nsView: TapHandlerView, context: Context) {
}
}
and use it
VStack {
Text("user: \(user)").font(.headline)
Divider()
}.background(Color.gray)
.overlay(TapHandler()) // << here !!
Tested with Xcode 12.0 / macOS 10.15.6
Im trying to make Augmented Reality app with RealityKit but ContentView.swift I have some problems
What is missing here ?` You can see errors on picture which I shared. I followed some tutorial so Im new on Xcode and Realitykit.
Failed to produce diagnostic for expression; please file a bug report
Cannot find 'PlacementButtonsView' in scope
import SwiftUI
import RealityKit
struct ContentView : View {
var models: [String] = {
let filemanager = FileManager.default
guard let path = Bundle.main.resourcePath, let files = try?
filemanager.contentsOfDirectory(atPath:path) else
{ return[]
}
var avaliableModels: [String] = []
for filename in files where filename.hasSuffix("usdz") {
let modelName = filename.replacingOccurrences(of: ".usdz", with: "")
avaliableModels.append(modelName)
}
return avaliableModels
}()
var body: some View {
ZStack(alignment: .bottom) {
ARViewContainer()
ModelPickerView(models: self.models)
PlacementButtonsView()
}
}
}
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
struct ModelPickerView: View {
var models: [String]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(0 ..<
self.models.count) { index in
Button(action: {
print("DEBUG: selected model with name: \(self.models[index])")
}) {
Image(uiImage: UIImage(named: self.models[index])!)
.resizable()
.frame(height: 60)
.aspectRatio(1/1,contentMode: .fit)
.background(Color.white)
.cornerRadius(12)
}
.buttonStyle (PlainButtonStyle())
}
}
}
.padding(15)
.background(Color.black.opacity(0.5))
}
struct PlacementButtonsView: View {
var body: some View {
HStack {
//Cancel Button
Button(action: {
print("DEBUG: model placement canceled.")
}) {
Image(systemName: "xmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
//Confirm Button
Button(action: {
print("DEBUG: model placement confirmed.")
}) {
Image(systemName: "checkmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.65))
.cornerRadius(30)
.padding(20)
}
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
}
Check your braces. The PlacementButtonsView struct is actually nested within the ModelPickerView struct, and not globally available, hence why it is not available from your ContentView.
By the way, in Xcode, you can find this out by option clicking on the declaration of PlacementButtonsView:
ModelPickerView.PlacementButtonsView shows you what went wrong here; PlacementButtonsView is nested within ModelPickerView. This is why you seem to have a strange closing brace on the final line of your code sample - the same issue occurs with the preview, as it is also nested in ModelPickerView.
To make this issue more visible, and see similar issues like this in the future more easily, you can also have Xcode indent your code for you by selecting all (Cmd + A) and then pressing Control + I. You'll see the PlacementButtonsView struct indent, making it more clear that it is not globally available.
How to achieve what the following Objective-C code achieves with SwiftUI? I haven't been able to get a firm grasp on the ideas presented.
[self presentViewController:messageViewController animated:YES completion:nil];
Until ios 13.x, there is no way provided by SwiftUI. As I had the same need, wrote a custom modifier of View to achieve it.
extension View {
func uiKitFullPresent<V: View>(isPresented: Binding<Bool>, style: UIModalPresentationStyle = .fullScreen, content: #escaping (_ dismissHandler: #escaping () -> Void) -> V) -> some View {
self.modifier(FullScreenPresent(isPresented: isPresented, style: style, contentView: content))
}
}
struct FullScreenPresent<V: View>: ViewModifier {
#Binding var isPresented: Bool
#State private var isAlreadyPresented: Bool = false
let style: UIModalPresentationStyle
let contentView: (_ dismissHandler: #escaping () -> Void) -> V
#ViewBuilder
func body(content: Content) -> some View {
if isPresented {
content
.onAppear {
if self.isAlreadyPresented == false {
let hostingVC = UIHostingController(rootView: self.contentView({
self.isPresented = false
self.isAlreadyPresented = false
UIViewController.topMost?.dismiss(animated: true, completion: nil)
}))
hostingVC.modalPresentationStyle = self.style
UIViewController.topMost?.present(hostingVC, animated: true) {
self.isAlreadyPresented = true
}
}
}
} else {
content
}
}
}
And, you can use it as the following.
.uiKitFullPresent(isPresented: $isShowingPicker, content: { closeHandler in
SomeFullScreenView()
.onClose(closeHandler) // '.onClose' is a custom extension function written. you can invent your own way to call 'closeHandler'.
})
content parameter of .uiKitFullPresent is a closure that has a callback handler as its parameter. you can use this callback to dismiss the presented view.
It's worked well so far. It looks a little bit tricky though.
As you may know, iOS 14 will bring us a method to present any view in the way you want. Check fullScreenCover() out.
Regarding presenting UIViewController written by Objective-C, it would be possible as Asperi mentioned in his post.
UPDATE
Here is the full source code I am using so far.
https://gist.github.com/fullc0de/3d68b6b871f20630b981c7b4d51c8373
UPDATE_2
Now, I'd like to say that it's not a good approach because the idea underlying doesn't actually seem to match well with the mechanism of SwiftUI.
As there is no provided related code, so in pseudo-code it would look like the following
struct YourParentView: View {
#State private var presented = false
var body: some View {
// some other code that activates `presented` state
SomeUIElement()
.sheet(isPresented: $presented) {
YourMessageViewControllerRepresentable()
}
}
}
I'm trying to add Touch Bar support for a SwiftUI View. There seems to be SwiftUI API for this using the .touchBar(content: () -> View) function on Views, but documentation is non existent and I can't get my Touch Bar to display anything.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.touchBar {
Button(action: {
}) {
Text("do something")
}
}
}
}
This code does compile and run, but the Touch Bar remains empty. How can I get my touch bar to display content using SwiftUI (not catalyst)?
Using .focusable doesn't work without "Use keyboard navigation to move focus between controls" checked in System Preferences -> Keyboard -> Shortcuts. To work around that, I did this:
/// Bit of a hack to enable touch bar support.
class FocusNSView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
}
/// Gets the keyboard focus if nothing else is focused.
struct FocusView: NSViewRepresentable {
func makeNSView(context: NSViewRepresentableContext<FocusView>) -> FocusNSView {
return FocusNSView()
}
func updateNSView(_ nsView: FocusNSView, context: Context) {
// Delay making the view the first responder to avoid SwiftUI errors.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
if let window = nsView.window {
// Only set the focus if nothing else is focused.
if let _ = window.firstResponder as? NSWindow {
window.makeFirstResponder(nsView)
}
}
}
}
}
Help from this How to use a SwiftUI touchbar with a NSWindow - Apple Developer Forums:
Use the focusable() modifier
The touch bar shows the text when you add the .focusable() modifier just before the .touchBar(content:) modifier.
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.focusable()
.touchBar {
Button(action: {
print("Perform some action")
}) {
Text("do something")
}
}
}
}