구현 목표
- 선택한 이미지나 텍스트를 삭제한다.
설명 및 소스 코드
- 선택된 이미지/텍스트가 있으면(activeId != nil) 이미지/텍스트를 배열에서 삭제한다.
- id를 UUID로 설정했기 때문에, imageInfos, textInfos, items 모두 id만을 비교해서 삭제하면 된다.
- 아이콘 메뉴는 아래에 붙인다. ignoreSafeArea를 적용하되, 아래쪽의 ignoreSafeArea 영역만큼은UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 을 사용하여 padding 처리한다.
ContentView.swift
import SwiftUI
struct ZoomState: Identifiable {
let id = UUID()
var scale: CGFloat = 1.0
var offset: CGSize = .zero
var gestureScale: CGFloat = 1.0
var gestureOffset: CGSize = .zero
var anchor: UnitPoint = .center
}
struct TextInfo {
var state = ZoomState()
// will do: var fontName: String = "SF Pro"
// will do: var weight: Font.Weight = .regular
}
struct ImageInfo {
var state = ZoomState()
// will do: var bright: CGFloat = .zero
// will do: var contrast: CGFloat = .zero
}
enum ItemAttr {
case image, text
}
struct Item {
var attr: ItemAttr
var id: UUID
}
struct ContentView: View {
@State private var imageInfos: [ImageInfo] = []
@State private var textInfos: [TextInfo] = []
@State private var items: [Item] = []
@State private var activeId: UUID?
@State private var canNewActive = true
func getItemAttrIndex(itemIndex: Int, itemAttr: ItemAttr) -> Int {
var index = -1
for i in 0...itemIndex {
if items[i].attr == itemAttr {
index += 1
}
}
return index
}
var body: some View {
VStack {
ZStack {
ForEach(items.indices, id: \.self) { index in
if items[index].attr == .image {
let imageIndex = getItemAttrIndex(itemIndex: index, itemAttr: .image)
if imageIndex != -1 {
ZoomImage(state: $imageInfos[imageIndex].state, activeId: $activeId, canNewActive: $canNewActive)
}
} else {
let textIndex = getItemAttrIndex(itemIndex: index, itemAttr: .text)
if textIndex != -1 {
ZoomText(state: $textInfos[textIndex].state, activeId: $activeId, canNewActive: $canNewActive)
}
}
}
}
.zIndex(-1)
HStack {
Image(systemName: "trash")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(8)
.background(.gray.opacity(0.2))
.clipShape(Circle())
.onTapGesture {
if activeId != nil {
imageInfos.removeAll(where: {activeId == $0.state.id})
textInfos.removeAll(where: {activeId == $0.state.id})
items.removeAll(where: {activeId == $0.id})
activeId = nil
}
}
Spacer()
HStack {
Image(systemName: "photo")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(8)
.onTapGesture {
let imageInfo = ImageInfo()
imageInfos.append(imageInfo)
items.append(Item(attr: .image, id: imageInfo.state.id))
activeId = imageInfo.state.id
}
Image(systemName: "textformat.size.larger")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(8)
.onTapGesture {
let textInfo = TextInfo()
textInfos.append(textInfo)
items.append(Item(attr: .text, id: textInfo.state.id))
activeId = textInfo.state.id
}
}
.padding(.horizontal)
.background(.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 24))
}
.padding()
.frame(maxWidth: .infinity)
.frame(height: 40)
.background(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0)
.ignoresSafeArea()
}
}
#Preview {
ContentView()
}
ZoomImage.swift
import SwiftUI
struct ZoomImage: View {
@Binding var state: ZoomState
@Binding var activeId: UUID?
@Binding var canNewActive: Bool
let size: CGSize = CGSize(width: 200, height: 200)
var body: some View {
GeometryReader { geometry in
ZStack {
let maxOffsetX = geometry.size.width / 2 / state.scale
let maxOffsetY = geometry.size.height / 2 / state.scale
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: size.width, height: size.height)
.border(.blue, width: activeId == state.id ? 2 : 0)
.offset(state.offset)
.scaleEffect(state.scale * state.gestureScale, anchor: state.anchor)
.gesture(
SimultaneousGesture(
DragGesture()
.onChanged { value in
if activeId == nil || canNewActive || activeId == state.id {
activeId = state.id
canNewActive = false
state.offset.width = min(max(state.offset.width + (value.translation.width - state.gestureOffset.width) / state.scale, -maxOffsetX), maxOffsetX)
state.offset.height = min(max(state.offset.height + (value.translation.height - state.gestureOffset.height) / state.scale, -maxOffsetY), maxOffsetY)
state.gestureOffset = value.translation
}
}
.onEnded { value in
state.gestureOffset = .zero
canNewActive = true
},
MagnifyGesture()
.onChanged({ value in
if activeId == nil || canNewActive || activeId == state.id {
canNewActive = false
activeId = state.id
state.anchor = value.startAnchor
state.gestureScale = value.magnification
}
})
.onEnded({ value in
state.scale *= value.magnification
state.gestureScale = 1.0
canNewActive = true
})
)
)
.onTapGesture {
if activeId == nil || canNewActive {
activeId = state.id
}
}
.animation(.interactiveSpring(), value: state.gestureOffset)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
}
#Preview {
ZoomImage(state: .constant(ZoomState()), activeId: .constant(nil), canNewActive: .constant(true))
}
ZoomText.swift
import SwiftUI
struct ZoomText: View {
@Binding var state: ZoomState
@Binding var activeId: UUID?
@Binding var canNewActive: Bool
var body: some View {
GeometryReader { geometry in
let maxOffsetX = geometry.size.width / 2
let maxOffsetY = geometry.size.height / 2
Text("Hello, development30years")
.font(.title)
.fixedSize()
.border(.blue, width: activeId == state.id ? 2 : 0)
.scaleEffect(state.scale * state.gestureScale)
.offset(state.offset)
.gesture(
SimultaneousGesture(
DragGesture()
.onChanged { value in
if activeId == nil || canNewActive || activeId == state.id {
activeId = state.id
canNewActive = false
state.offset.width = min(max(state.offset.width + (value.translation.width - state.gestureOffset.width), -maxOffsetX), maxOffsetX)
state.offset.height = min(max(state.offset.height + (value.translation.height - state.gestureOffset.height), -maxOffsetY), maxOffsetY)
state.gestureOffset = value.translation
}
}
.onEnded { value in
state.gestureOffset = .zero
canNewActive = true
},
MagnifyGesture()
.onChanged({ value in
if activeId == nil || canNewActive || activeId == state.id {
activeId = state.id
canNewActive = false
state.gestureScale = value.magnification
}
})
.onEnded({ value in
state.scale *= value.magnification
state.gestureScale = 1.0
canNewActive = true
})
)
)
.onTapGesture {
if activeId == nil || canNewActive {
activeId = state.id
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
#Preview {
ZoomText(state: .constant(ZoomState()), activeId: .constant(nil), canNewActive: .constant(true))
}
'SwiftUI' 카테고리의 다른 글
이미지/텍스트 편집기: 텍스트 속성 편집(8) (0) | 2024.12.12 |
---|---|
이미지/텍스트 편집기: 이미지 속성 편집(7) (0) | 2024.12.12 |
이미지/텍스트 편집기: 선택한 이미지/텍스트 외곽선 표시(5) (0) | 2024.12.11 |
이미지/텍스트 편집기: 메뉴에서 이미지와 텍스트 생성(4) (0) | 2024.12.11 |
이미지/텍스트 편집기: 화면 범위내 텍스트와 이미지 확대 /축소, 이동(3) (0) | 2024.12.09 |