SwiftUI - navigationBarBackButton - navigationbar

How can I change navigationBarBackButton color in SwiftUI? (by
default it's blue)
How can I change navigationBarBackButton text in SwiftUI?
This is my code (an example):
struct ExercisesList : View {
var exercises : [Exercise]
var body: some View {
NavigationView {
List(self.exercises.identified(by: \.number)) {exercise in
NavigationLink(destination: ExerciseDetailView(exercise: exercise)) {
ExerciseRow(exercisE: exercise)
}
}
.navigationBarTitle(Text("Exercises"))
}
}
}
By default, the navigationBarBackButton in "ExerciseDetailView" has a text of Exercise and color of blue.
My question is how can I manipulate these two?

Here is a solution. Hope it will help. In this way, you can use custom UIColor also.
struct ExercisesList : View {
var exercises : [Exercise]
var body: some View {
UINavigationBar.appearance().tintColor = UIColor.red
return NavigationView {
List(self.exercises.identified(by: \.number)) { exercise in
NavigationLink(destination: ExerciseDetailView(exercise: exercise)) {
ExerciseRow(exercisE: exercise)
}
}
}
.navigationBarTitle(Text("Exercises"))
}
}

Currently, No direct methods are available for that(XCode 11 beta 3).
However You can use UINavigationBar method for that,
see below code :
struct ExercisesList : View {
var exercises : [Exercise]
init() {
//navigationBarBackButton color will change//
UINavigationBar.appearance().tintColor = .purple
// you can also set backgroundColor//
UINavigationBar.appearance().backgroundColor = .white
}
var body: some View {
NavigationView {
List(self.exercises.identified(by: \.number)) {exercise in
NavigationLink(destination: ExerciseDetailView(exercise: exercise)) {
ExerciseRow(exercisE: exercise)
}
}
.navigationBarTitle(Text("Exercises"))
}
}
}

Related

sheet not showing when presented from button in Menu - SwiftUI

Still some what new to SwiftUI. Now I'm trying to present a sheet from a button in a Menu. I can reproduce the issue with the sample code below:
import SwiftUI
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Press to dismiss") {
presentationMode.wrappedValue.dismiss()
}
.font(.title)
.padding()
.background(Color.black)
}
}
struct TestButtonInMenu: View {
#State private var showingSheet = false
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
}
enum SampleEnum: String, CaseIterable {
case one, two, three, four
}
struct ContentView: View {
var body: some View {
Form {
Section {
VStack {
ForEach(SampleEnum.allCases, id:\.self) { id in
Menu("\(Text(id.rawValue))") {
TestButtonInMenu()
}
}
}
}
}
}
}
I've tried different sheet initializers but they don't make a difference.
What am I missing? Is this possible in SwiftUI?
You have a couple of problems with the code. First of all, in your ContentView you have the Menu inside of the ForEach. By doing it that way, you have created four menus with one button each, instead of one menu with four buttons. The point of Menu is to hide the buttons until they are needed.
The second issue is that you are trying to show one sheet off the button that is buried in another view in the menu. The sheet really should be declared in the parent, not a child, and I think you have confused the OS. That being said, I think eventually you intend to call four different sheets from the different buttons, and the answer Asperi pointed you to will help as you will be calling different sheets from the one .sheet. I corrected the code and just brought the button into the main UI and out of its own struct.
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Press to dismiss") {
presentationMode.wrappedValue.dismiss()
}
.font(.title)
.padding()
.background(Color.black)
}
}
enum SampleEnum: String, CaseIterable {
case one, two, three, four
}
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
Form {
Section {
VStack {
Menu("Show Sheet") {
ForEach(SampleEnum.allCases, id:\.self) { id in
Button(id.rawValue) {
showingSheet.toggle()
}
}
}
}
}
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
}

Navigation Title not showing

So at first I wanted to get rid of the Navigation bar from the home screen as it does not want to show the title & I don't really need it.
However I can't remove it because I use NavigationLink. So I am left in a little pickle.
How do I either hide the toolbar/navigation bar on home page or show the title.
MIN CODE
struct ContentView: View {
var body: some View {
NavigationView {
Text("Loading")
}.navigationBarTitle(Text("Home"))
}
}
So after a few more attempts seems that the title had to be shown in
var body: some View {
NavigationView {
if posts.isEmpty {
Text("Loading")
//.navigationBarTitle("") //this must be empty
.navigationBarHidden(true)
} else {
(REST OF CODE)
}
}
}
Still can't hide it though. The issue is it keeps disappearing when the if statement runs.
The .navigationBarTitle, as well as other similar modifiers, should be used inside NavigationView, like
struct ContentView: View {
var body: some View {
NavigationView {
Text("Loading")
.navigationBarTitle(Text("Home")) // << here !!
}
}
}
Update: if you want to make modifier work for both condition branches wrap condition into some container (VStack, Group, etc.), like
var body: some View {
NavigationView {
Group {
if posts.isEmpty {
Text("Loading")
} else {
(REST OF CODE)
}
}
//.navigationBarTitle("") //this must be empty
.navigationBarHidden(true) // << here !!
}
}

Getting SwiftUI wrapper of AVPlayer to pause when view disappears

TL;DR
Can't seem to use binding to tell wrapped AVPlayer to stop — why not? The "one weird trick" from Vlad works for me, without state & binding, but why?
See Also
My question is something like this one but that poster wanted to wrap an AVPlayerViewController and I want to control playback programmatically.
This guy also wondered when updateUIView() was called.
What happens (Console logs shown below.)
With code as shown here,
The user taps "Go to Movie"
MovieView appears and the vid plays
This is because updateUIView(_:context:) is being called
The user taps "Go back Home"
HomeView reappears
Playback halts
Again updateUIView is being called.
See Console Log 1
But... remove the ### line, and
Playback continues even when the home view returns
updateUIView is called on arrival but not departure
See Console log 2
If you uncomment the %%% code (and comment out what precedes it)
You get code I thought was logically and idiomatically correct SwiftUI...
...but "it doesn't work". I.e. the vid plays on arrival but continues on departure.
See Console log 3
The code
I do use an #EnvironmentObject so there is some sharing of state going on.
Main content view (nothing controversial here):
struct HomeView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
ZStack() { // +++ Weird trick ### fails if this is Group(). Wtf?
if router.page == .home {
Button(action: { self.router.page = .movie }) {
Text("Go to Movie")
}
} else if router.page == .movie {
MovieView()
}
}
}
}
which uses one of these (still routine declarative SwiftUI):
struct MovieView: View {
#EnvironmentObject var router: ViewRouter
// #State private var isPlaying: Bool = false // %%%
var body: some View {
VStack() {
PlayerView()
// PlayerView(isPlaying: $isPlaying) // %%%
Button(action: { self.router.page = .home }) {
Text("Go back Home")
}
}.onAppear {
print("> onAppear()")
self.router.isPlayingAV = true
// self.isPlaying = true // %%%
print("< onAppear()")
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
// self.isPlaying = false // %%%
print("< onDisappear()")
}
}
}
Now we get into the AVKit-specific stuff. I use the approach described by Chris Mash.
The aforementioned PlayerView, the wrappER:
struct PlayerView: UIViewRepresentable {
#EnvironmentObject var router: ViewRouter
// #Binding var isPlaying: Bool // %%%
private var myUrl : URL? { Bundle.main.url(forResource: "myVid", withExtension: "mp4") }
func makeUIView(context: Context) -> PlayerView {
PlayerUIView(frame: .zero , url : myUrl)
}
// ### This one weird trick makes OS call updateUIView when view is disappearing.
class DummyClass { } ; let x = DummyClass()
func updateUIView(_ v: PlayerView, context: UIViewRepresentableContext<PlayerView>) {
print("> updateUIView()")
print(" router.isPlayingAV = \(router.isPlayingAV)")
// print(" isPlaying = \(isPlaying)") // %%%
// This does work. But *only* with the Dummy code ### included.
// See also +++ comment in HomeView
if router.isPlayingAV { v.player?.pause() }
else { v.player?.play() }
// This logic looks reversed, but is correct.
// If it's the other way around, vid never plays. Try it!
// if isPlaying { v?.player?.play() } // %%%
// else { v?.player?.pause() } // %%%
print("< updateUIView()")
}
}
And the wrappED UIView:
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer?
init(frame: CGRect, url: URL?) {
super.init(frame: frame)
guard let u = url else { return }
self.player = AVPlayer(url: u)
self.playerLayer.player = player
self.layer.addSublayer(playerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
required init?(coder: NSCoder) { fatalError("not implemented") }
}
And of course the view router, based on the Blckbirds example
class ViewRouter : ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter, Never>()
enum Page { case home, movie }
var page = Page.home { didSet { objectWillChange.send(self) } }
// Claim: App will never play more than one vid at a time.
var isPlayingAV = false // No didSet necessary.
}
Console Logs
Console log 1 (playing stops as desired)
> updateUIView() // First call
router.isPlayingAV = false // Vid is not playing => play it.
< updateUIView()
> onAppear()
< onAppear()
> updateUIView() // Second call
router.isPlayingAV = true // Vid is playing => pause it.
< updateUIView()
> onDisappear() // After the fact, we clear
< onDisappear() // the isPlayingAV flag.
Console log 2 (weird trick disabled; playing continues)
> updateUIView() // First call
router.isPlayingAV = false
< updateUIView()
> onAppear()
< onAppear()
// No second call.
> onDisappear()
< onDisappear()
Console log 3 (attempt to use state & binding; playing continues)
> updateUIView()
isPlaying = false
< updateUIView()
> onAppear()
< onAppear()
> updateUIView()
isPlaying = true
< updateUIView()
> updateUIView()
isPlaying = true
< updateUIView()
> onDisappear()
< onDisappear()
Well... on
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
print("< onDisappear()")
}
this is called after view is removed (it is like didRemoveFromSuperview, not will...), so I don't see anything bad/wrong/unexpected in that subviews (or even it itself) is not updated (in this case updateUIView)... I would rather surprise if it would be so (why update view, which is not in view hierarchy?!).
So this
class DummyClass { } ; let x = DummyClass()
is rather some wild bug, or ... bug. Forget about it and never use such stuff in releasing products.
OK, one would now ask, how to do with this? The main issue I see here is design-originated, specifically tight-coupling of model and view in PlayerUIView and, as a result, impossibility to manage workflow. AVPlayer here is not part of view - it is model and depending on its states AVPlayerLayer draws content. Thus the solution is to tear apart those entities and manage separately: views by views, models by models.
Here is a demo of modified & simplified approach, which behaves as expected (w/o weird stuff and w/o Group/ZStack limitations), and it can be easily extended or improved (in model/viewmodel layer)
Tested with Xcode 11.2 / iOS 13.2
Complete module code (can be copy-pasted in ContentView.swift in project from template)
import SwiftUI
import Combine
import AVKit
struct MovieView: View {
#EnvironmentObject var router: ViewRouter
// just for demo, but can be interchangable/modifiable
let playerModel = PlayerViewModel(url: Bundle.main.url(forResource: "myVid", withExtension: "mp4")!)
var body: some View {
VStack() {
PlayerView(viewModel: playerModel)
Button(action: { self.router.page = .home }) {
Text("Go back Home")
}
}.onAppear {
self.playerModel.player?.play() // << changes state of player, ie model
}.onDisappear {
self.playerModel.player?.pause() // << changes state of player, ie model
}
}
}
class PlayerViewModel: ObservableObject {
#Published var player: AVPlayer? // can be changable depending on modified URL, etc.
init(url: URL) {
self.player = AVPlayer(url: url)
}
}
struct PlayerView: UIViewRepresentable { // just thing wrapper, as intended
var viewModel: PlayerViewModel
func makeUIView(context: Context) -> PlayerUIView {
PlayerUIView(frame: .zero , player: viewModel.player) // if needed viewModel can be passed completely
}
func updateUIView(_ v: PlayerUIView, context: UIViewRepresentableContext<PlayerView>) {
}
}
class ViewRouter : ObservableObject {
enum Page { case home, movie }
#Published var page = Page.home // used native publisher
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer?
init(frame: CGRect, player: AVPlayer?) { // player is a model so inject it here
super.init(frame: frame)
self.player = player
self.playerLayer.player = player
self.layer.addSublayer(playerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
required init?(coder: NSCoder) { fatalError("not implemented") }
}
struct ContentView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
Group {
if router.page == .home {
Button(action: { self.router.page = .movie }) {
Text("Go to Movie")
}
} else if router.page == .movie {
MovieView()
}
}
}
}

TornadoFX:proper way to bind model

I was taking a look at this :
tornadofx
and tried to expand on it with database connection and little more options, (not all of them make sense, but its just playing in a sandbox).
Even though table can be directly edited and the data will persist in database, i did try to do edit through text fields too. actual table editing would happen through different view and not table itself, as i said its just example.
Database used is Jetbrains Exposed.
object Categories : IntIdTable() {
val name = varchar("name", 64).uniqueIndex()
val description = varchar("description", 128)
}
class Category(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Category>(Categories)
var name by Categories.name
var description by Categories.description
override fun toString(): String {
return "Category(name=\"$name\", description=\"$description\")"
}
}
now controller looks something like this, functions are just rudimentary and picked as an example.
typealias ModelToDirtyState = Map.Entry<CategoryModel, TableColumnDirtyState<CategoryModel>>
class CategoryModel() : ItemViewModel<Category>() {
val name: SimpleStringProperty = bind(Category::name)
val description: SimpleStringProperty = bind(Category::description)
}
class DBController : Controller() {
val categories: ObservableList<CategoryModel> by lazy {
transaction {
SchemaUtils.create(Categories)
Category.all().map {
CategoryModel().apply {
item = it
}
}.observable()
}
}
init {
Database.connect(
"jdbc:mysql://localhost:3306/test", driver = "com.mysql.cj.jdbc.Driver",
user = "test", password = "test"
)
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
}
fun deleteCategory(model: CategoryModel) {
runAsync {
transaction {
model.item.delete()
}
}
categories.remove(model)
}
fun updateCategory(model: CategoryModel) {
transaction {
Categories.update {
model.commit()
}
}
}
fun commitDirty(modelDirtyMappings: Sequence<ModelToDirtyState>) {
transaction {
modelDirtyMappings.filter { it.value.isDirty }.forEach {
it.key.commit()
println(it.key)// commit value to database
it.value.commit() // clear dirty state
}
}
}
Just to quickly comment on controller, delete method works as "intended" however the update one does not, it does not work in sense that after using delete item is remove both from database and tableview(underlying list) itself, and when i do update its not, now i know the reason, i call remove manually on both database and list, now for update perhaps i could do change listener, or maybe tornadofx can do this for me, i just cant set it up to do it. Following code will make things clearer i think.
class CategoryEditor : View("Categories") {
val categoryModel: CategoryModel by inject()
val dbController: DBController by inject()
var categoryTable: TableViewEditModel<CategoryModel> by singleAssign()
var categories: ObservableList<CategoryModel> by singleAssign()
override val root = borderpane {
categories = dbController.categories
center = vbox {
buttonbar {
button("Commit") {
action {
dbController.commitDirty(categoryTable.items.asSequence())
}
}
button("Roll;back") {
action {
categoryTable.rollback()
}
}
// This model only works when i use categorytable.tableview.selected item, if i use categoryModel, list gets updated but not the view itself
// Question #1 how to use just categoryModel variable without need to use categorytable.tableview.selecteditem
button("Delete ") {
action {
val model = categoryTable.tableView.selectedItem
when (model) {
null -> return#action
else -> dbController.deleteCategory(model)
}
}
}
//And here no matter what i did i could not make the view update
button("Update") {
action {
when (categoryModel) {
null -> return#action
else -> dbController.updateCategory(categoryModel)
}
categoryTable.tableView.refresh()
}
}
}
tableview<CategoryModel> {
categoryTable = editModel
items = categories
enableCellEditing()
enableDirtyTracking()
onUserSelect() {
//open a dialog
}
//DOES WORK
categoryModel.rebindOnChange(this) { selectedItem ->
item = selectedItem?.item ?: CategoryModel().item
}
// Question #2. why bindSelected does not work, and i have to do it like above
//DOES NOT WORK
// bindSelected(categoryModel)
//
column("Name", CategoryModel::name).makeEditable()
column("Description", CategoryModel::description).makeEditable()
}
}
right = form {
fieldset {
field("Name") {
textfield(categoryModel.name)
}
}
fieldset {
field("Description") {
textfield(categoryModel.description)
}
}
button("ADD CATEGORY") {
action {
dbController.addCategory(categoryModel.name.value, categoryModel.description.value)
}
}
}
}
}
I apologize for huge amount of code, also in last code snipped i left questions in form of comments where i fail to achive desired results.
I am sure i am not properly binding code, i just dont see why, also i sometimes use one variable to update data, my declared one "categoryModel" and sometimes i use tableview.selecteditem, it just seems hacky and i cant seem to grasp way.
Thank you!

How to override signal handler of superclass component

I have a base class item something like this:
Base.qml:
Item {
Thing {
id: theThing;
onMySignal: { console.log("The signal"); }
}
}
And I'm trying to make a derived item - Derived.qml.
How can I override the onMySignal handler of theThing? I have tried stuff like this...
Derived.qml:
Base {
theThing.onMySignal: { console.log("Do different things with theThing in Derived") }
}
but I can't find anything to tell me how to express this syntactically correctly, or whether/how to actually go about it!
You can define the code of the signal handler as a property in superclass and override it in the derived item:
Item {
property var handlerCode: function () { console.log("In Superclass") };
Thing {
id: theThing;
onMySignal: handlerCode()
}
}
Overriding :
Base {
handlerCode: function () { console.log("In Derived Item!") };
}

Resources