UIViewRepresentable protocl을 활용해야 합니다
UIViewRepresentable를 따르는 struct를 만들어서 SwiftUI에서 바로 사용하면 됩니다.
이 struct는 반드시 두 가지 메소드를 구현해야 합니다.
final class RedLabel: UILabel {
override func awakeFromNib() {
super.awakeFromNib()
textColor = .red
}
}
struct RepresentableRedLabel: UIViewRepresentable {
var text: String
let redLabel = RedLabel()
func makeUIView(context: Context) -> UILabel {
redLabel
}
func updateUIView(_ uiView: UILabel, context: Context) {
redLabel.text = text
}
}
struct ContentView: View {
var body: some View {
RepresentableRedLabel(text: "내용")
}
}
// UILabel의 RedLabel을 SwiftUI에서 사용하는 경우입니다.
UIViewController를 SwiftUI에서 사용하기 위해서는 UIViewControllerRepresentable가 필수입니다.
UIViewControllerRepresentable를 구현하기 위해서는 반드시 두 가지 메소드를 구현해야합니다.
– func makeUIViewController(context: Context) -> some UIViewController
– func updateUIViewController(_ uiViewController: some UIViewController, context: Context)
struct ImagePickerController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIImagePickerController{
UIImagePickerController()
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
}
struct ContentView: View {
var body: some View {
ImagePickerController()
}
}
UIImgePickerController에서 선택한 UIImage를 Coordinator를 사용해서 SwiftUI에 표시하는 코드
struct ImagePickerController: UIViewControllerRepresentable {
// SwiftUI 에서의 부모뷰의 @State property부터의 Binding
@Binding var selectedImage: Image?
@Binding var existSelectedImage: Bool
func makeUIViewController(context: Context) -> UIImagePickerController {
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = context.coordinator
return imagePickerController
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
// Cordinator 대입.
func makeCoordinator() -> Coordinator {
Coordinator(selectedImage: $selectedImage, existSelectedImage: $existSelectedImage)
}
}
extension ImagePickerController {
// Cordinator 만들기
final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding var selectedImage: Image?
@Binding var existSelectedImage: Bool
// 밑줄을 사용해서 초기화 해야한다. 프로퍼티 래퍼임을 알려주기 위해서.
init(selectedImage: Binding<Image?>, existSelectedImage: Binding<Bool>) {
_selectedImage = selectedImage
_existSelectedImage = existSelectedImage
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedOriginalImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }
selectedImage = Image(uiImage: selectedOriginalImage)
existSelectedImage = true
}
}
}
struct ContentView: View {
@State private var selectedImage: Image?
@State private var existSelectedImage = false
var body: some View {
ZStack {
VStack {
selectedImage?
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100.0, height: 100.0)
Button(action: didTapSelectedImageButton) {
Text("Select Image")
}
}
if selectedImage == nil {
ImagePickerController(
selectedImage: $selectedImage,
existSelectedImage: $existSelectedImage
)
}
}
}
private func didTapSelectedImageButton() {
selectedImage = nil
existSelectedImage = false
}
}
// @State property와 Binding으로 Coordinator에서 부모 뷰에 업데이트를 별도로 하지 않아도 간단하게 구현함
기존에 UIView 또는 UIViewController에서 SwiftUI View를 사용할 때에는 UIViewRepresentable을 따르는 Struct를 매번 따로 만들어 줘야할필요 없이 @Bindng할 property가 필요없는 경우에는 UIKit class에서 extension으로 구현이 가능하다.
final class Label: UILabel {}
// Extension 으로 정의
extension Label: UIViewRepresentable {
func makeUIView(context: Context) -> UILabel {
backgroundColor = .gray
text = "This is UIKit UILabel"
return self
}
func updateUIView(_ uiView: UILabel, context: Context) {}
}
struct ContentView: View {
var body: some View {
VStack {
Text("This is Swift UI Text")
Label()
}
}
}
UIHostingController를 사용해야 합니다.
별도로 추가해야하는 method나 property는 없습니다.
UIHostingController(rootView: UIKit에서 표시할 SwiftUI): SwiftUI View를 초기화해서 넣어주기만 하면 끝
struct ContentView: View {
var body: some View {
Text("내용")
.fontWeight(.bold)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let hostingController = UIHostingController(rootView: ContentView())
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.frame = view.bounds
addChild(hostingController)
view.addSubview(hostingController.view)
}
}
UIHostingController의 rootView로 SwiftUI를 넣어주기만 하면 된다. addChild, addSubView는 지금까지 UIKit에서 UIViewController를 추가시킬 때와 같다.
struct ContentView: View {
var body: some View {
Text("펭 - 하!")
.fontWeight(.bold)
}
}
class ViewController: UIViewController {
@IBAction private func didTapPresentButton(_ sender: Any) {
let hostingController = UIHostingController(rootView: ContentView())
present(hostingController, animated: true)
}
}
SwiftUI 화면을 present 시킬 때도 SwiftUI 뷰를 UIHostingController로 감싸는 것 이외의 UIKit 코드는 지금까지 동일하게 사용하면 된다.
struct ContainerView<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
content
}
}
// 뷰에 단순히 넣는것 뿐만 아니라, if-else, switch-case 블럭에서도 사용 가능하다.
struct SimpleView: View {
var body: some View {
ContainerView{
Text("SimpleView Text")
}
}
}
struct IfElseView: View{
var flag = true
var body: some View {
ContainerView{
if flag {
Text("True text")
} else {
Text("False text")
}
}
}
}
struct SwitchCaseView: View {
var condition = 1
var body: some View {
ContainerView {
switch condition {
case 1:
Text("one")
default:
Text("Default")
}
}
}
}
struct ParentView: View {
var body: some View {
NavigationView{
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
}
struct ChildView<Content: View>: View {
var destinationView: Content
var title: String
init(destinationView: Content, title: String) {
self.destinationView = destinationView
self.title = title
}
var body: some View {
NavigationLink(destination: destinationView){
Text("This item opens the \(title) view").foregroundColor(Color.black)
}
}
}
SwiftUI에서 UITableView는 List입니다.
UIKit에서 사용했던 UITableViewDelegate, UITableViewDataSource 없이 간단히 구현이 가능하다.
struct ContentView: View {
var body: some View {
List {
Text("1")
Text("2")
Text("3")
}
}
}
따로 커스텀해서 항목을 추가하는것도 간단히 가능하다
struct CustomCell: View {
var body: some View {
HStack {
Image(systemName: "tortoise.fill")
Text("거북이")
}
}
}
struct ContentView: View {
var body: some View {
List {
CustomCell()
Text("1")
Text("2")
Text("3")
}
}
}
List가 네비게이션뷰의 하위 뷰로 선언되어있고 Cell이 네비게이션 링크로 감싸져있다면, Cell을 터치하면 다른 뷰로 이동이 가능하다.
List에 표시될 Array의 내용물은 반드시 Identifiable 프로토콜을 따르는 class, struct로 정의 해야한다
struct Item: Identifiable {
var id = UUID() // Int, String 등 hashable을 따르는 것들은 뭐든지 가능
let name: String
}
List 내에서 식별될 수 있는 id 프로퍼티도 반드시 가지고 있어야 한다 !
struct ContentView: View {
let items: [Item] = [Item(name: "asdf"), Item(name: "sadfasf")]
var body: some View {
NavigationView {
List(items) {item in
Text(item.name)
}
.listStyle(GroupedListStyle())
.navigationBarTitle("List View")
}
}
}
우선 items가 상태 프로퍼티로 존재해야한다. 따라서 위의 코드의 items를 @State private var items
로 바꿔주고 아래 코드처럼 함수를 추가해주면 된다.
List {
// ForEach로 각각의 cell에 delete 메소드를 입력시켜주어야한다.
ForEach(items) { item in
Text(item.name)
}.onDelete(perform: didDeleteCell)
}
.listStyle(GroupedListStyle())
.navigationBarTitle("List View")
// parameter 에는 반드시 offsets 을 추가해야 함.
func didDeleteCell(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
print("Deleted cell is \(offsets)")
}
수정 코드(셀의 순서 변경 가능)
List {
ForEach(items) { item in
Text(item.name)
}.onMove(perform: didMoveCell)
}
.listStyle(GroupedListStyle())
.navigationBarTitle("List View")
.navigationBarItems(trailing: EditButton()) // 수정 버튼이 들어갈 Item
func didMoveCell(form source: IndexSet, to destination: Int) {}
참조
https://unnnyong.com/2020/05/22/swiftui-list-%ea%b5%ac-uitableview/