I am writing a music player app using apple Media Player and have a button to change the volume and hide the system default volume overlay.
However, all methods I find are based on UIKit.
Like
let volumeView = MPVolumeView(frame: .zero)
volumeView.clipsToBounds = true
volumeView.alpha = 0.00001
volumeView.showsVolumeSlider = false
view.addSubview(volumeView)
And I tried
import Foundation
import SwiftUI
import MediaPlayer
struct VolumeView:UIViewRepresentable{
let volumeView:MPVolumeView
func makeUIView(context: Context) -> MPVolumeView {
volumeView.clipsToBounds = true
volumeView.alpha = 0.00001
volumeView.showsVolumeSlider = false
return volumeView
}
func updateUIView(_ uiView: MPVolumeView, context: Context) {
}
typealias UIViewType = MPVolumeView
}
It receives the MPVolumeView I created in my view model and I place it in a swiftui view. The indicator disappears but it can't change the volume.
Then I tried to make a new instance of MPVolumeView in UIViewRepresentable and it also didn't work.
I am a green hand in swiftui, can anybody help me?
I had the same question and your example helped me to figure it out, you were nearly there apparently.
So here is a complete custom Volume Slider in SwiftUI
struct VolumeSliderView: View {
var primaryColor: Color
#StateObject var volumeViewModel = VolumeViewModel()
var body: some View {
ZStack {
VolumeView()
.frame(width: 0, height: 0)
Slider(value: $volumeViewModel.volume, in: 0...1) {
Text("")
} minimumValueLabel: {
Image(systemName: "speaker.fill")
.font(.callout)
.foregroundColor(primaryColor.opacity(0.6))
} maximumValueLabel: {
Image(systemName: "speaker.wave.3.fill")
.font(.callout)
.foregroundColor(primaryColor.opacity(0.6))
} onEditingChanged: { isEditing in
volumeViewModel.setVolume()
}
.tint(primaryColor.opacity(0.6))
}
}
}
import Foundation
import MediaPlayer
struct VolumeView: UIViewRepresentable{
func makeUIView(context: Context) -> MPVolumeView {
let volumeView = MPVolumeView(frame: CGRect.zero)
volumeView.alpha = 0.001
return volumeView
}
func updateUIView(_ uiView: MPVolumeView, context: Context) {
}
}
It seems that an instance of MPVolumeView needs to be present on the screen at any time, even if it's not visible. Seems a bit hacky but works. If I only hide the view in the setVolume() function and even call volumeView.showsVolumeSlider = false there, it won't make a difference.
class VolumeViewModel: ObservableObject {
#Published var volume: Float = 0.5
init() {
setInitialVolume()
}
private func setInitialVolume() {
volume = AVAudioSession().outputVolume
}
func setVolume() {
let volumeView = MPVolumeView()
let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.03) {
slider?.setValue(self.volume, animated: false)
}
}
}
Hope it helps somebody, as I just found non working examples here on SO.
I want to navigate to a custom UIView where the system edge gestures are disabled. I am using the SwiftUI life cycle with UIViewControllerRepresentable and overriding preferredScreenEdgesDeferringSystemGestures.
I have seen the solutions with SceneDelegates. Does preferredScreenEdgesDeferringSystemGestures have to act on window.rootViewController for it to work?
class MyUIViewController: UIViewController {
typealias UIViewControllerType = MyUIViewController
open override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
return [.all];
}
let labelDescription: UILabel = {
let label = UILabel()
label.text = "But it's not working."
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(labelDescription)
labelDescription.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
}
}
struct UIViewControllerRepresentation : UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
let uiViewController = MyUIViewController()
return uiViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("To UIView with no system edge gestures.",
destination: UIViewControllerRepresentation())
.navigationTitle("Title")
}
}
}
https://developer.apple.com/documentation/uikit/uiviewcontroller/2887511-childforscreenedgesdeferringsyst
If I understand it correctly the system only asks the first UIViewController and if that vc doesn't return a child that the system should ask too then that's it.
Since you don't have access to the view controllers in SwiftUI (or even know what types of view controllers it will use) I opted to just swizzle the childForScreenEdgesDeferringSystemGestures and childForHomeIndicatorAutoHidden getters and return the view controller that manages these for me by looping over all the UIViewController.children.
Since you linked to this question from my Gist I will link back there for the solution which is specific to my Gist. https://gist.github.com/Amzd/01e1f69ecbc4c82c8586dcd292b1d30d
I've been trying to follow a tutorial on embedding an AVCaptureVideoPreviewLayer (or in simpler terms a very basic viewfinder using camera) and have had trouble converting the iOS code to fit my MacOS application.
The issue appeared when I attempted to convert this block of code:
// setting view for preview...
struct CameraPreview: UIViewRepresentable {
#ObservedObject var camera : CameraModel
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.frame = view.frame
// Your Own Properties...
camera.preview.videoGravity = .resizeAspectFill
view.layer.addSublayer(camera.preview)
// starting session
camera.session.startRunning()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
My understanding is that UIViews are only for iOS development and that MacOS uses NSViews instead. Knowing this I did my best to come up with the following modified code
// setting view for preview...
struct CameraPreview: NSViewRepresentable { // ERROR #1
#ObservedObject var camera : CameraModel
func makeNSView(context: Context) -> NSView {
let view = NSView(frame: CGRect(origin: .zero,
size: CGSize(width: 200, height: 200)))
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.frame = view.frame
// Your Own Properties...
camera.preview.videoGravity = .resizeAspectFill
view.addSubview(camera.preview) //ERROR #2
// starting session
camera.session.startRunning()
return view
}
func updateUIView(_ uiView: NSView, context: NSView) {
}
}
The issues are:
Type 'CameraPreview' does not conform to protocol 'NSViewRepresentable' (ERROR #1)
Cannot convert value of type 'AVCaptureVideoPreviewLayer' to expected argument type 'NSView' (ERROR #2)
I'm an intermediate iOS developer, but have been trying to build my skills in MacOS development.
You forgot to update this part updateUIView
func updateNSView(_ uiView: NSView, context: Context) { // << here !!
}
about second... you try to add layer to view, but layout should be added to layer, like
let view = NSView(frame: CGRect(origin: .zero,
size: CGSize(width: 200, height: 200)))
view.wantsLayer = true // needed explicitly for NSView
...
view.layer.addSublayer(camera.preview)
Using a List view, is there a way to access (and therefore modify) the underlying UITableView object without reimplementing the entire List as a UIViewRepresentable?
I've tried initializing a List within my own UIViewRepresentable, but I can't seem to get SwiftUI to initialize the view when I need it to, and I just get an empty basic UIView with no subviews.
This question is to help find an answer for Bottom-first scrolling in SwiftUI.
Alternatively, a library or other project that reimplements UITableView in SwiftUI would also answer this question.
The answer is Yes. There's an amazing library that lets you inspect the underlying UIKit views. Here's a link to it.
The answer is no. As of iOS 13, SwiftUI's List is not currently designed to replace all the functionality and customizability of UITableView. It is designed to meet the most basic use of a UITableView: a standard looking, scrollable, editable list where you can place a relatively simply view in each cell.
In other words, you are giving up customizability for the simplicity of having swipes, navigation, moves, deletes, etc. automatically implemented for you.
I'm sure that as SwiftUI evolves, List (or an equivalent view) will get more customizable, and we'll be able to do things like scroll from the bottom, change padding, etc. The best way to make sure this happens is to file feedback suggestions with Apple. I'm sure the SwiftUI engineers are already hard at work designing the features that will appear at WWDC 2020. The more input they have to guide what the community wants and needs, the better.
I found a library called Rotoscope on GitHub (I am not the author of this).
This library is used to implement RefreshUI also on GitHub by the same author.
How it works is that Rotoscope has a tagging method, which overlays a 0 sized UIViewRepresentable on top of your List (so it's invisible). The view will dig through the chain of views and eventually find the UIHostingView that's hosting the SwiftUI views. Then, it will return the first subview of the hosting view, which should contains a wrapper of UITableView, then you can access the table view object by getting the subview of the wrapper.
The RefreshUI library uses this library to implement a refresh control to the SwiftUI List (you can go into the GitHub link and check out the source to see how it's implemented).
However, I see this more like a hack than an actual method, so it's up to you to decide whether you want to use this or not. There are no guarantee that it will continue working between major updates as Apple could change the internal view layout and this library will break.
You can Do it. But it requires a Hack.
Add Any custom UIView
Use UIResponder to backtrack until you find table View.
Modify UITableView The way you like.
Code Example of Adding Pull to refresh:
//1: create a custom view
final class UIKitView : UIViewRepresentable {
let callback: (UITableView) -> Void
init(leafViewCB: #escaping ((UITableView) -> Void)) {
callback = leafViewCB
}
func makeUIView(context: Context) -> UIView {
let view = UIView.init(frame: CGRect(x: CGFloat.leastNormalMagnitude,
y: CGFloat.leastNormalMagnitude,
width: CGFloat.leastNormalMagnitude,
height: CGFloat.leastNormalMagnitude))
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let superView = uiView.superview {
superView.backgroundColor = uiView.backgroundColor
}
if let tableView = uiView.next(UITableView.self) {
callback(tableView)
}
}
}
extension UIResponder {
func next<T: UIResponder>(_ type: T.Type) -> T? {
return next as? T ?? next?.next(type)
}
}
////Use:
struct Result: Identifiable {
var id = UUID()
var value: String
}
class RefreshableObject: ObservableObject {
let id = UUID()
#Published var items: [Result] = [Result(value: "Binding"),
Result(value: "ObservableObject"),
Result(value: "Published")]
let refreshControl: UIRefreshControl
init() {
refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action:
#selector(self.handleRefreshControl),
for: .valueChanged)
}
#objc func handleRefreshControl(sender: UIRefreshControl) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
sender.endRefreshing()
self?.items = [Result(value:"new"), Result(value:"data"), Result(value:"after"), Result(value:"refresh")]
}
}
}
struct ContentView: View {
#ObservedObject var refreshableObject = RefreshableObject()
var body: some View {
NavigationView {
Form {
Section(footer: UIKitView.init { (tableView) in
if tableView.refreshControl == nil {
tableView.refreshControl = self.refreshableObject.refreshControl
}
}){
ForEach(refreshableObject.items) { result in
Text(result.value)
}
}
}
.navigationBarTitle("Nav bar")
}
}
}
Screenshot:
To update from refresh action, binding isUpdateOrdered is being used.
this code is based on code I found in web, couldn't find the author
import Foundation
import SwiftUI
class Model: ObservableObject{
#Published var isUpdateOrdered = false{
didSet{
if isUpdateOrdered{
update()
isUpdateOrdered = false
print("we got him!")
}
}
}
var random = 0
#Published var arr = [Int]()
func update(){
isUpdateOrdered = false
//your update code.... maybe some fetch request or POST?
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
LegacyScrollViewWithRefresh(isUpdateOrdered: $model.isUpdateOrdered) {
VStack{
if model.arr.isEmpty{
//this is important to fill the
//scrollView with invisible data,
//in other case scroll won't work
//because of the constraints.
//You may get rid of them if you like.
Text("refresh!")
ForEach(1..<100){ _ in
Text("")
}
}else{
ForEach(model.arr, id:\.self){ i in
NavigationLink(destination: Text(String(i)), label: { Text("Click me") })
}
}
}
}.environmentObject(model)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct LegacyScrollViewWithRefresh: UIViewRepresentable {
enum Action {
case idle
case offset(x: CGFloat, y: CGFloat, animated: Bool)
}
typealias Context = UIViewRepresentableContext<Self>
#Binding var action: Action
#Binding var isUpdateOrdered: Bool
private let uiScrollView: UIScrollView
private var uiRefreshControl = UIRefreshControl()
init<Content: View>(isUpdateOrdered: Binding<Bool>, content: Content) {
let hosting = UIHostingController(rootView: content)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
self._isUpdateOrdered = isUpdateOrdered
uiScrollView = UIScrollView()
uiScrollView.addSubview(hosting.view)
let constraints = [
hosting.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor),
hosting.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor),
hosting.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
]
uiScrollView.addConstraints(constraints)
self._action = Binding.constant(Action.idle)
}
init<Content: View>(isUpdateOrdered: Binding<Bool>, #ViewBuilder content: () -> Content) {
self.init(isUpdateOrdered: isUpdateOrdered, content: content())
}
init<Content: View>(isUpdateOrdered: Binding<Bool>, action: Binding<Action>, #ViewBuilder content: () -> Content) {
self.init(isUpdateOrdered: isUpdateOrdered, content: content())
self._action = action
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
uiScrollView.addSubview(uiRefreshControl)
uiRefreshControl.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl(arguments:)), for: .valueChanged)
return uiScrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
switch self.action {
case .offset(let x, let y, let animated):
uiView.setContentOffset(CGPoint(x: x, y: y), animated: animated)
DispatchQueue.main.async {
self.action = .idle
}
default:
break
}
}
class Coordinator: NSObject {
let legacyScrollView: LegacyScrollViewWithRefresh
init(_ legacyScrollView: LegacyScrollViewWithRefresh) {
self.legacyScrollView = legacyScrollView
}
#objc func handleRefreshControl(arguments: UIRefreshControl){
print("refreshing")
self.legacyScrollView.isUpdateOrdered = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
arguments.endRefreshing()
//refresh animation will
//always be shown for 2 seconds,
//you may connect this behaviour
//to your update completion
}
}
}
}
There is currently no way to access or modify the underlying UITableView
I just wanted to create a view and when it shown then the whole background will be dimmed like an alert view controller. If it is possible then please guide me and if possible then provide me code.
Thank you
The simplest way for doing that is to add a semi-transparent background (e.g. black with alpha less than 1.0) view, which contains the alert view. The background view should cover all other views in the view controller.
You can also use a modal view controller which has such a background view as its view, and presenting this controller with presentation style Over Full Screen.
// Here is the wrapper code i use in most of my project now a days
protocol TransparentBackgroundProtocol {
associatedtype ContainedView
var containedNib: ContainedView? { get set }
}
extension TransparentBackgroundProtocol where ContainedView: UIView {
func dismiss() {
containedNib?.superview?.removeFromSuperview()
containedNib?.removeFromSuperview()
}
mutating func add(withFrame frame: CGRect, toView view: UIView, backGroundViewAlpha: CGFloat) {
containedNib?.frame = frame
let backgroundView = configureABlackBackGroundView(alpha: backGroundViewAlpha)
view.addSubview(backgroundView)
guard let containedNib = containedNib else {
print("No ContainedNib")
return
}
backgroundView.addSubview(containedNib)
}
private func configureABlackBackGroundView(alpha: CGFloat) -> UIView {
let blackBackgroundView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
blackBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(alpha)
return blackBackgroundView
}
}
// Sample View shown like alertView
class LogoutPopUpView: UIView, TransparentBackgroundProtocol {
// MARK: Variables
weak var containedNib: LogoutPopUpView?
typealias ContainedView = LogoutPopUpView
// MARK: Outlets
// MARK: Functions
class func initiate() -> LogoutPopUpView {
guard let nibView = Bundle.main.loadNibNamed("LogoutPopUpView", owner: self, options: nil)?[0] as? LogoutPopUpView else {
fatalError("Cann't able to load nib file.")
}
return nibView
}
}
// where u want to show pop Up
logOutPopup = LogoutPopUpView.instanciateFromNib()
let view = UIApplication.shared.keyWindow?.rootViewController?.view {
logOutPopup?.add(withFrame: CGRect(x: 30, y:(UIScreen.main.bounds.size.height-340)/2, width: UIScreen.main.bounds.size.width - 60, height: 300), toView: view, backGroundViewAlpha: 0.8)
}
// for dismiss
self.logOutPopup?.dismiss()