Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
muukii committed Mar 5, 2023
1 parent 42c8ed4 commit 18f3d8a
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
32 changes: 32 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SwiftUIHosting",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SwiftUIHosting",
targets: ["SwiftUIHosting"]
)
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SwiftUIHosting",
dependencies: []
),
.testTarget(
name: "SwiftUIHostingTests",
dependencies: ["SwiftUIHosting"]
),
]
)
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# MyLibrary

A description of this package.
82 changes: 82 additions & 0 deletions Sources/SwiftUIHosting/HostingController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import SwiftUI

@available(iOS 13, *)
final class HostingController<Content: View>: UIHostingController<Content> {

var onViewDidLayoutSubviews: (UIViewController) -> Void = { _ in }

init(disableSafeArea: Bool, rootView: Content) {
super.init(rootView: rootView)

// https://www.notion.so/muukii/UIHostingController-safeArea-issue-ec66a560970c4a1cb44f21cc448bc513?pvs=4
#if USE_SWIZZLING
_ = _once_
_fixing_safeArea = disableSafeArea
#else
_disableSafeArea = disableSafeArea
#endif
}

@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

onViewDidLayoutSubviews(self)
}
}

#if USE_SWIZZLING

private let _once_: Void = {
UIView.replace()
}()

private var _key: Void?

extension UIView {

fileprivate static func replace() {

method_exchangeImplementations(
class_getInstanceMethod(self, #selector(getter:UIView.safeAreaInsets))!,
class_getInstanceMethod(self, #selector(getter:UIView._hosting_safeAreaInsets))!
)

method_exchangeImplementations(
class_getInstanceMethod(self, #selector(getter:UIView.safeAreaLayoutGuide))!,
class_getInstanceMethod(self, #selector(getter:UIView._hosting_safeAreaLayoutGuide))!
)

}

fileprivate var _fixing_safeArea: Bool {
get {
(objc_getAssociatedObject(self, &_key) as? Bool) ?? false
}
set {
objc_setAssociatedObject(self, &_key, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}

@objc dynamic var _hosting_safeAreaInsets: UIEdgeInsets {
if _fixing_safeArea {
return .zero
} else {
return self._hosting_safeAreaInsets
}
}

@objc dynamic var _hosting_safeAreaLayoutGuide: UILayoutGuide? {
if _fixing_safeArea {
return nil
} else {
return self._hosting_safeAreaLayoutGuide
}
}

}

#endif
167 changes: 167 additions & 0 deletions Sources/SwiftUIHosting/SwiftUIHostingView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import SwiftUI

/// A view that hosts SwiftUI for UIKit environment.
open class SwiftUIHostingView: UIView {

public struct Configuration {

/**
Registers internal hosting controller into the nearest view controller's children.

Apple developer explains why we need to UIHostinController should be a child of the view controller.
```
When using UIHostingController, make sure to always add the view controller together with the view to your app.

Many SwiftUI features, such as toolbars, keyboard shortcuts, and views that use UIViewControllerRepresentable, require a connection to the view controller hierarchy in UIKit to integrate properly, so never separate a hosting controller's view from the hosting controller itself.
```
*/
public var registersAsChildViewController: Bool

/**
Fixes handling safe area issue
https://www.notion.so/muukii/UIHostingController-safeArea-issue-ec66a560970c4a1cb44f21cc448bc513?pvs=4
*/
public var disableSafeArea: Bool

public init(
registersAsChildViewController: Bool = true,
disableSafeArea: Bool = true
) {
self.registersAsChildViewController = registersAsChildViewController
self.disableSafeArea = disableSafeArea
}
}

private var hostingController: HostingController<RootView>

private let proxy: Proxy = .init()

public let configuration: Configuration

public convenience init<Content: View>(
_ name: String = "",
_ file: StaticString = #file,
_ function: StaticString = #function,
_ line: UInt = #line,
configuration: Configuration = .init(),
@ViewBuilder content: @escaping () -> Content
) {
self.init(
name,
file,
function,
line,
configuration: configuration
)
setContent(content: content)
}

// MARK: - Initializers

public init(
_ name: String = "",
_ file: StaticString = #file,
_ function: StaticString = #function,
_ line: UInt = #line,
configuration: Configuration = .init()
) {
self.configuration = configuration

self.hostingController = HostingController(
disableSafeArea: configuration.disableSafeArea,
rootView: RootView(proxy: proxy)
)

super.init(frame: .null)

#if DEBUG
let file = URL(string: file.description)?.deletingPathExtension().lastPathComponent ?? "unknown"
self.accessibilityIdentifier = [
name,
file,
function.description,
line.description,
]
.joined(separator: ".")
#endif

hostingController.view.backgroundColor = .clear

addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: topAnchor),
hostingController.view.rightAnchor.constraint(equalTo: rightAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
hostingController.view.leftAnchor.constraint(equalTo: leftAnchor),
])

hostingController.onViewDidLayoutSubviews = { controller in
// TODO: Reduces number of calling invalidation, it's going to be happen even it's same value.
controller.view.invalidateIntrinsicContentSize()
}

}

@available(*, unavailable)
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: UIView

/// Returns calculated size using internal hosting controller
open override func sizeThatFits(_ size: CGSize) -> CGSize {
hostingController.sizeThatFits(in: size)
}

open override func didMoveToWindow() {

super.didMoveToWindow()

if configuration.registersAsChildViewController {
// https://muukii.notion.site/Why-we-need-to-add-UIHostingController-to-view-controller-chain-14de20041c99499d803f5a877c9a1dd1

if let _ = window {
if let parentViewController = self.findNearestViewController() {
parentViewController.addChild(hostingController)
hostingController.didMove(toParent: parentViewController)
} else {
assertionFailure()
}
} else {
hostingController.willMove(toParent: nil)
hostingController.removeFromParent()
}
}
}

// MARK: -

public final func setContent<Content: SwiftUI.View>(
@ViewBuilder content: @escaping () -> Content
) {
proxy.content = {
return SwiftUI.AnyView(
content()
)
}
}

}

final class Proxy: ObservableObject {
@Published var content: () -> SwiftUI.AnyView? = { nil }

init() {
}
}

struct RootView: SwiftUI.View {
@ObservedObject var proxy: Proxy

var body: some View {
proxy.content()
}
}
12 changes: 12 additions & 0 deletions Sources/SwiftUIHosting/UIResponder+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import UIKit

extension UIResponder {

func findNearestViewController() -> UIViewController? {
sequence(first: self, next: { $0.next })
.first {
$0 is UIViewController
} as? UIViewController
}

}
1 change: 1 addition & 0 deletions Tests/SwiftUIHostingTests/MyLibraryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

0 comments on commit 18f3d8a

Please sign in to comment.