This solution is adapted from responses (primarily Mojtaba Hosseini) to a couple of similar questions on StackOverflow and also from Alan Quatermain's well written blog titled "SwiftUI Bindings with CoreData".
I use this solution in a macOS target (with Core Data) and an
@ObservedObject var. I've adapted this answer to work independently with an
@State var. Please be aware that I have not attempted to build and run this with an iOS target yet, but it works in the SwiftUI canvas in Xcode 12 beta 1 and using live preview for the macOS target.
Essentially
TextEditor and
Text views must be drawn in the same way - must have identical characteristics, e.g.
.font modifier, etc. The
Text view "ghosts" the
TextEditor and performs two roles:
presents the placeholder text when the Optional<String> (@State private var objectDescription: String?) is nil.
dynamically expands the height of the stacked views to accommodate multiple lines of text in the TextEditor.
If anyone
understands the mechanics behind what makes the "ghosting" technique work in the
ZStack, please add an answer, because I'm still trying to figure that out.
Note that
TextEditor seems to be a little unstable in Xcode 12 beta 1 under macOS 11 beta 2. I include the
.frame modifier to ensure the
TextEditor view presents itself as I expect it to - without this modifier it occasionally and inexplicably "disappears".
Code Block public extension Binding where Value: Equatable { |
init(_ source: Binding<Value?>, replacingNilWith nilProxy: Value) { |
self.init( |
get: { source.wrappedValue ?? nilProxy }, |
set: { newValue in |
if newValue == nilProxy { |
source.wrappedValue = nil |
} |
else { |
source.wrappedValue = newValue |
} |
}) |
} |
} |
Code Block struct YourSwiftUIView: View { |
@State private var objectDescription: String? |
var body: some View { |
VStack(alignment: .leading) { |
let placeholder = "enter detailed Description" |
Text("Description") |
ZStack(alignment: .topLeading) { |
TextEditor(text: Binding($objectDescription, replacingNilWith: "")) |
.frame(minHeight: 30, alignment: .leading) |
// following line is a hack to force TextEditor to appear |
// similar to .textFieldStyle(RoundedBorderTextFieldStyle())... |
.cornerRadius(6.0) |
.foregroundColor(Color(.labelColor)) |
.multilineTextAlignment(.leading) |
Text(objectDescription ?? placeholder) |
// following line is a hack to create an inset similar to the TextEditor inset... |
.padding(.leading, 5) |
.foregroundColor(Color(.placeholderTextColor)) |
.opacity(objectDescription == nil ? 1 : 0) |
} |
.font(.body) |
} |
} |
} |