SwiftUI View 1 (Text, Image, Layout)

Updated:

🔷 UIkit vs SwiftUI

  • UIkit 을 대응으로 구송요소들과 SwiftUI 요소 들과 SwiftUI 의 요소들을 한눈에 비교하는 표 입니다

👉 Views and Controls

UIKit SwiftUI
UILabel Text
UITextField TextField
UITextField(isSecure) SecureField
UIButton Button
UIImageView Image
UISwitch Toggle
UISlider Slider
UIStepper Stepper
UIPickerView Picker(WheelPickerStyle)
UISegmentedControl Picker(SegmentedPickerStyle)
UIDatePicker DatePicker
UITextView 동일 요소 없음
UIStackView HStack(가로), VStack(세로)
UIScrollView ScrollView
UITableView(Plain) List
UITableView(Grouped) List(GroupedListStyle)
UICollectionView 동일 요서 없음

👉 ViewController

UIKit SwiftUI
UIViewController View
UINavigationController NavigationView
UITabBarController TabView
UISpritController NavigationView
UITableViewController List
UICollectionViewController 동일요소 없음
UIAlertController(actionSheet 스타일) ActionSheet
UIAlertController(alert 스타일) Alert

🔷 Text

Text 는 UIKit 의 UILabel 의 역활을 하는데 SwiftUI 에서는 Button, Picker, Toggle 에서도 Text 를 사용하기 때문에 가장 기본적이면서도 많이 쓰이는 뷰가 됩니다

	// 텍스트에 제공되는 여러 가지 수식어를 이용해 쉽게 문자열을 조작할 수 있습니다
struct Example01: View {
  var body: some View {
    VStack(spacing: 30) { // 세로방향 뷰를 배열하는 stackView
      Text("폰트와 굵기 설정")
        .font(.title) // 폰트 설정
        .fontWeight(.black) // 폰트 굵기

      Text("글자색은 foreground, 배경은 background")
        .foregroundColor(.white) // 글자 색
        .padding() // 텍스트 주변 여백 설정
        .background(Color.blue) // 텍스트의 배경 설정(Color 명시)

      Text("커스텀 폰트, 볼드체, 이탤릭체, 밑줄, 취소선")
        .font(.custom("Menlo", size: 16)) // 커스텀 폰트 설정
        .bold() // 볼드체
        .italic() // 이탤릭체
        .underline() // 밑줄
        .strikethrough() // 취소선

      Text("라인 수 제한과 \n 텍스트 정렬 기능입니다. \n 이건 안보여야 됩니다 2줄까지만..")
        .lineLimit(2) // 텍스트를 최대 2줄 까지만 표현
        .multilineTextAlignment(.center) // 다중행 문자열의 정렬 방식 지정
        .fixedSize() // 주어진 공간의 크기가 작아도 텍스트를 생략하지 않고 표현되도록 설정

      // 2개 이상의 텍스트를 하나로 묶어서 동시에 적용할 수 있습니다
      (Text("자간과 기준선").kerning(8) // 자간
        + Text("조정도 쉽게 가능합니다").baselineOffset(8)) // 기준선
        .font(.system(size: 19))
    }
  }
}

스크린샷

🔶 수식어 적용 순서 유의 사항

  • 뷰에 수식어를 적용할 때는 유의해야 할 점들이 있습니다. 각각의 뷰는 그 자체가 가진 수식어와 뷰 프로토콜이 가진 수식어로 나뉩니다.

  • 이름은 동일하지만 텍스트에 정의된 수식어는 반환 타입이 Text 이고, 뷰 프로토콜에 정의된 수식어는 반환 타입니 some View 와 같이 서로 다른 것을 볼 수 있습니다

Text("SwiftUI")
  .font(.title) // Text - 호출자의 타입이 Text 이므로 다시 반환
  .bold() // Text
  .padding() // View - padding 수식어 호출 이후에는 Text 가 아닌 View 반환

Text("SwiftUI")
  .bold() // Text
  .padding() // View - padding 수식어 호출 이후에는 Text 가 아닌 View 반환
  .font(.title) // View - 동일한 font 수식어를 호출해도 호출자에 따라 반환 타입이 다릅니다
Text("SwiftUI")
  .padding() // View
  .bold() // 컴파일 오류 : 뷰 프로토콜에는 bold 수식어가 없으므로 오류가 발생합니다
  .font(.title)
Text("SwiftUI")
  .padding() // View
  .font(.title) // View
  .bold() // 컴파일 오류: 뷰 프로토콜에는 bold 수식어가 없습니다
  • 수식어는 이전의 뷰를 감싼 새로운 뷰를 만들어 내고, 그다음 수식어는 다시 그 뷰를 감쌉니다. 그래서 수식어를 적용하는 순서에 따라 결과가 많이 달라 집니다
// 수식어 적용 순서에 따라 실행 결과가 달라질 수 있습니다
struct Example02: View {
  var body: some View {
    VStack(spacing: 20) {
      Text("😀 🥰 🤗 😶‍🌫️")
        .font(.largeTitle)
        .background(Color.blue)  // background 우선 적용
        .padding()

      Text("😀 🥰 🤗 😶‍🌫️")
        .font(.largeTitle)
        .padding() // padding 우선 적용
        .background(Color.blue)
    }
  }
}

스크린샷

🔷 Image

  • Image 는 UIKit 의 UIImageView 와 같이 저장된 이미지를 표현하는 뷰이지만, 사용법은 SwiftUI 에서 간편해 졌습니다.

  • SwiftUI 에서 이미지를 다룰때는 이미지는 기본적으로 주어진 공간과 관계없이 그 고유의 크기를 유지합니다. 그래서 이지지 크기는 변하지 않고 뷰가 차지 하는 공간만 달라져서 아래와 같이 겹쳐서 나타 나게 됩니다

// 프레임 수식어만으로는 이미지의 크기가 변화하지 않습니다.
struct Example01: View {
  var body: some View {
    HStack {
      Image("SwiftUI") // 원본 100 X 100 pt
      Image("SwiftUI").frame(width: 50, height: 50) // frame 만으로 사이즈가 달라지지 않습니다
      Image("SwiftUI").frame(width: 200, height: 200) // 사이즈 변동 없음
    }
  }
}

스크린샷

👉 Resizable

  • 이미지의 크기르 ㄹ변경하는 경우 resizable 수식어를 적용해야 합니다. resizable 은 이미지에서만 사용 가능한 수식어 인데 frame 수식어보다 먼저 적용이 되야 합니다

  • resizable 수식어는 이미지 타입에 선언되어 있는 것으로 뷰 프로토콜이 반환되는 수식어를 사용하기 전에 호출해야 한다는 것입니다. frame 과 resizable 순서가 바뀌면 오류가 방생됩니다

// resizable 수식어는 부모 뷰가 제공하는 공간에 맞게 이미지 크기를 조정합니다.
// 비율과 크기는 콘텐트 모드에 따라 달라질 수 있습니다
struct Example02: View {
  var body: some View {
    VStack(spacing: 80) {
      HStack {
        Image("SwiftUI")
        Image("SwiftUI").resizable().frame(width: 50, height: 50)
        Image("SwiftUI").resizable().frame(width: 200, height: 200)
      }

      HStack{
        // capInset 매개 변수에 늘어날 영역 지정 .resizingMode 생략 시 stretch 적용
        Image("SwiftUI").resizable(capInsets: .init(top: 0, leading: 50, bottom: 0, trailing: 0)).frame(width: 150, height: 150)
        Image("SwiftUI").resizable(resizingMode: .tile).frame(width: 150, height: 150)
      }
    }
  }
}

스크린샷

👉 ContentMode

  • resizable 를 적용하면 자신이 주어진 공간 내에서 최대한의 크기만큼 확장하려는 합니다. 단 비율에 신경 쓰지 않고 공간을 가득 채우로고 함 그래서 ContentMode 를 사용하여 비율을 조절합니다
UIKit SwiftUI 설명
Scale to Fill 기본값 비율과 관계없이 이미지를 늘려서 주어진 공간을 가득 채우게 합니다
Aspect Fit .scaleToFit() 이미지 원본의 비유을 그대로 유지한 상태에서, 가능한 최대 크기까지 늘어 납니다. 이때 최대 크기는 주어진 공간의 너비와 높이 중 작은 값을 기준으로 합니다
Aspect Fill .scaleToFill() 이미지 원본의 비율을 그대로 유지한 상태에서, 가능한 최대 크기까지 늘어납니다. 이때, 최대 크기는 주어진 공간의 너비와 높이 중 큰 값을 기준으로 합니다. 따라서 이미지의 일부가 주어진 공간을 벗어나 더 크게 표현될 수 있습니다
// scaledToFit, scaledToFill 수식어를 이용해 콘텐츠 모드를 변경할 수 있습니다.
struct Example03: View {
  var body: some View {
    HStack(spacing: 30) {
      // UIKit의 Scale To Fill 이 기본값으로 적용
      Image("SwiftUI").resizable().frame(width: 100, height: 150)

      // UIKit의 Aspect Fit 효과 적용
      Image("SwiftUI").resizable().scaledToFit().frame(width: 100, height: 150)

      // UIKit 의 Aspect Fill 효과 적용
      Image("SwiftUI").resizable()
        .scaledToFill()
        .frame(width: 100, height: 150)
        .clipped() // UIView의 ClipsToBounds 활성화. 프레임을 벗어나는 이미지 제거

    }
  }
}

스크린샷

👉 AspectRatio

  • 이미지의 비율에 대해 좀 더 세붖덕인 조정이 필요하다면 aspectRatio 수식어를 사용해서 이미지의 너비와 노ㅠ이의 비율을 조정합니다

  • parameter 으로 CGFloat(너비 / 높이를 계산해서 그 비율을 전달), CGSize(width 와 height 에 각각 원하는 값을 입력합니다)

// aspectRatio를 통해 세부적인 비율 조정이 가능합니다.
struct Example04: View {
  var body: some View {
    // scaleToFit 콘텐츠 모드를 적용한 뒤, 너비가 높이보다 1.6배의 비율을 가지도록 조정
    HStack(spacing: 30) {
      Image("SwiftUI").resizable()
        .aspectRatio(CGSize(width: 1.6, height: 1), contentMode: .fit)
        .frame(width: 150, height: 150)

    // scaledtoFill 콘텐츠 모드 적용한 뒤, 너비가 높이보다 0.7배의 비율을 가지도록 조정
      Image("SwiftUI").resizable()
        .aspectRatio(0.7, contentMode: .fill)
        .frame(width: 150, height: 150).clipped()
    }
  }
}

스크린샷

👉 ClipShape

  • clipShape 수식어를 이용해 이미지를 원하는 모양으로 만들 수도 있습니다. 도형의 크기는 이미지의 크기를 기준으로 생성되지만 원하는 크기로 조정하는 것도 가능합니다
// clipShape - 지정한 Shape 의 모습으로 이미지 잘라내기
struct Example05: View {
  var body: some View {
    HStack(spacing: 20) {
      Image("SwiftUI").clipShape(Circle()) // 원
      Image("SwiftUI").clipShape(Rectangle().inset(by: 10)) // 이미지 크기보다 사방으로 10씩 크기를 줄인 사각형
      // 크기와 위치를 직접 지정한 타원
      Image("SwiftUI").clipShape(Ellipse().path(in: CGRect(x: 10, y: 10, width: 80, height: 110)))
    }
  }
}

스크린샷

👉 RenderingMode

  • 이미지에는 template 과 original 이렇게 2가지 랜더일 모드가 사용되니다

    • template : 이미지의 불투명 영역이 가진 본래의 색을 무시하고 원하는 색으로 변경해 템플릿 이미지로 활용할 수 있게 합니다

    • original : 항상 이미지 본래의 색을 유지합니다

// original / template 렌더링 모드를 지정해 줄 수 있습니다.
struct Example06: View {
  var body: some View {
    HStack {
      Image("SwiftUI") // 랜더링 모드 생략 시 시스템이 결정
      Image("SwiftUI").renderingMode(.original) // 원본 이미지 색상 유지
      Image("SwiftUI").renderingMode(.template) // template 색상으로 변경
    }
    .foregroundColor(.red)
  }
}

스크린샷

👉 SF Symbols

  • SF Symbols 는 백터 기반의 이미지로 만들어져 이미지의 색을 쉽게 변경할 수 있으며, 크기 변화에 자유롭습니다
// SF Symbol은 Image(systemName:) 생성자를 사용합니다.
struct Example07: View {
  var body: some View {
    HStack(spacing: 20) {
      Image(systemName: "star.circle").resizable().frame(width: 100, height: 100)
      Image(systemName: "star.circle.fill").resizable().frame(width: 100, height: 100)
    }
  }
}

스크린샷

👉 ImageScale

  • 각 심벌은 imageScale 수식어를 적용해서 small, medium, large 를 선택 할 수 있습니다. 기본값은 medium 이고, 각 심벌에 원하는 색을 입힐 수 도 있습니다i
// imageScale을 이용하면 크기를 변경할 수 있습니다.
struct Example08: View {
  var body: some View {
    HStack(spacing: 30) {
      Image(systemName: "book.fill").imageScale(.small).foregroundColor(.red)
      Image(systemName: "book.fill").foregroundColor(.green) // medium 기본값
      Image(systemName: "book.fill").imageScale(.large).foregroundColor(.blue)
    }
  }
}

스크린샷

👉 Font

  • ImageScale 이외에도 font 를 이용해 크기를 변경하는 것도 가능합니다
// font를 이용해 심벌의 크기를 조절 가능합니다.
struct Example09: View {
  var body: some View {
    HStack(spacing: 30) {
      Image(systemName: "speaker.3").font(.body)
      Image(systemName: "speaker.3").font(.title)
      Image(systemName: "speaker.3").font(.system(size: 40))
      Image(systemName: "speaker.3").imageScale(.large).font(.system(size: 40))
    }
  }
}

스크린샷

👉 Weight

  • 이미지에는 굴기를 바로 조정할 수가 없어서 font 를 먼저 사용해서 변경해야 합니다
// fontWeight를 이용해 심벌의 굵기를 조절 가능합니다.
struct Example10: View {
  var body: some View {
    HStack(spacing: 30) {
      Image(systemName: "arrow.up").font(Font.title.weight(.black))
      Image(systemName: "arrow.left").font(Font.title.weight(.semibold))
      Image(systemName: "arrow.right").font(Font.title.weight(.light))
      Image(systemName: "arrow.down").font(Font.title.weight(.ultraLight))
    }
  }
}

스크린샷

🔷 View Layout

👉 Stack

  • UIkit 의 UIStackView 와 같은 역활로써 SwiftUI 에서는 필수적으로 활용되는 중요합니다

  • 가로방향(HStack), 세로방향(VStack), 계층방향(ZStack) 와 같이 3가지 종류의 스택이 있습니다

// HStack, VStack, ZStack 세 가지 종류의 스택이 있습니다.
struct Example01: View {
  var body: some View {
    VStack(spacing: 50){
      HStack {
        Rectangle().fill(Color.green).frame(width: 100, height: 100)
        Rectangle().fill(Color.yellow).frame(width: 100, height: 100)
      }
      VStack {
        Rectangle().fill(Color.green).frame(width: 100, height: 100)
        Rectangle().fill(Color.yellow).frame(width: 100, height: 100)
      }
      ZStack {
        Rectangle().fill(Color.green).frame(width: 100, height: 100)
	Rectangle().fill(Color.yellow).frame(width: 100, height: 100).offset(x: 40, y: 40)
      }
    }
  }
}

스크린샷

spacing, argument

  • spacing 을 매개변수의 값을 조정하여 뷰의 간격을 지정해 줄 수 있습니다.

  • argument 를 통해 자식 뷰의 정렬 위치를 지정할 수 있습니다

// alignment 매개 변수를 통해 자식 뷰의 정렬 위치를 지정할 수 있습니다.
struct Example02: View {
  var body: some View {
    HStack(alignment: .top) {
      Rectangle().fill(Color.green).frame(width: 150, height: 150)
      Rectangle().fill(Color.yellow).frame(width: 150, height: 550) // heigth 를
    }
  }
}

스크린샷

수식어 활용

  • 스택에도 뷰 프로토콜이 가진 수식어를 적용할 수 있습니다.
// 컨테이너 뷰 자체에 영향을 미치는 수식어가 있고 자식 뷰에게 반영되는 수식어가 있습니다.
struct Example03: View {
  var body: some View {
    HStack {
      Text("HStack").font(.title).foregroundColor(Color.blue)
      Text("은 뷰를 가로로 배열합니다")
      Text("!")
    }.padding().border(Color.black) // 여백을 확보하고 검은색 테두리 그리기
  }
}

스크린샷

스택 조합해서 도형만들기

// 여러 가지 스택을 조합하여 원하는 레이아웃을 구성할 수 있습니다.
struct Example04: View {
  var body: some View {
    VStack{
      Text("도형 만들기").font(.largeTitle).fontWeight(.heavy)

      HStack {
        Text("둥근 모양").font(.title)
        Spacer() // 가장 좌측에 배치되도록 하기
      }

      ZStack {
        Rectangle().frame(height: 10) // 사각형의 높이를 10으로 주어 선으로 사용
        HStack{
          Circle().fill(Color.yellow) // 원
          Ellipse().fill(Color.green) // 타원
          Capsule().fill(Color.orange) // 캡슐
          RoundedRectangle(cornerRadius: 30).fill(Color.gray) // 둥근 모서리 사각형
        }
      }

      HStack{
        Text("각진 모양").font(.title)
        Spacer()
      }

      ZStack {
        Rectangle().frame(height: 10)
        HStack {
          Color.red // SwiftUI 에서는 컬러 그 자체도 하나의 뷰에 해당합니다
          Rectangle().fill(Color.blue) // 사각형
          RoundedRectangle(cornerRadius: 0).fill(Color.purple) // CornerRadius 를 0으로 준 사각형
        }
      }
    }.padding()
  }
}

스크린샷

👉 Spacer

  • Spacer 는 뷰 사이의 간격을 설정하거나 뷰의 크기를 확장할 용도로 사용되는 레이아웃을 위한 뷰입니다
	// Spacer 는 가능한 최대 크기만큼 공간을 확보하지만, frame 으로 크기를 제한 할 수 있습니다
	struct Example01: View {
		var body: some View {
			VStack(spacing: 50) {

				HStack {
					Spacer() // 최대크기만큼 늘림
					Text("Spacer").font(.title).background(Color.yellow)
				}.background(Color.blue)

				HStack {
					Spacer().frame(width: 100) // spacer 크기 조절
					Text("Spacer").font(.title).background(Color.yellow)
				}.background(Color.blue)
			}
		}
	}

스크린샷

  • spacer 는 사용 가능한 뷰의 크기에 따라 간격을 알아서 조절하는데, minLength 의 값을 설정하면 최소 간격을 지정할 수 있습니다. 더 큰 크기로 늘어나는 건 관계 없으나 더 작아 지지는 않습니다
	// minLength 매개 변수로 Spacer의 최소 길이를 지정할 수 있습니다.
	struct Example02: View {
		var body: some View {
			VStack (spacing: 50) {
				HStack {
					Text("Spacer MinLength").font(.title).foregroundColor(.white)
					Spacer(minLength: 100) // minLength 100
					Text("Spacer").font(.title).background(Color.yellow)
				}.background(Color.blue)

				HStack {
					Text("Spacer MinLength").font(.title).foregroundColor(.white)
					Spacer() // minLength 미지정
					Text("Spacer").font(.title).background(Color.yellow)
				}.background(Color.blue)
			}
		}
	}

스크린샷

  • 단, ZStack 에서 Spacer 는 다른 형제 뷰의 최대 크기만큼만 확장되기 때문에, ZStack 에서는 Cole.clear 나 Rectangle 처럼 부모뷰의 크기만큼 커지려는 확장성을 가진 뷰를 spacer 대신 사용할 수 있습니다
// ZStack에서 사용된 Spacer는 스택 외부에서 단독으로 사용되었을 때와 동일하게 동작합니다.
// ZStack에서 공간을 확장하기 위해서는 Spacer 대신 Color.clear, Rectangle() 등을 사용할 수 있습니다.
struct Example03: View {
  var body: some View {
    ZStack {
      Color.clear
      Text("Spacer").font(.title).background(Color.yellow)
    }.background(Color.blue)
  }
}

스크린샷 2021-12-31 오전 11 25 53

👉 Overlay and Background

  • Overlay 와 Background 수식어를 사용하면 ZStack 처럼중첩된 뷰를 표현하는것이 가능합니다

Overlay

  • Overlay 는 뷰 원본으 ㅣ공간을 기준으로 그 위헤 새로운 뷰를 중첩하여 쌓는 기능을 합니다. UIKit 에서 addSubView 메서드를 사용하는 개념과 같은데, 자식 뷰를 추가하면 부모 뷰를 기준으로 프레임의 좌표가 결정되고 그 크기도 영향을 받았던 것과 동일 합니다
// MARK: Example 01
// overlay 는 뷰의 상위 계층에 중첩하여 쌓는 수식어 입니다.
struct Example01: View {
  var body: some View {
    Rectangle().fill(Color.green).frame(width: 150, height: 150, alignment: .center)
      .overlay(Rectangle().fill(Color.yellow).offset(x: 20, y: 20)) // 초록색 위로 사각형 위로 뷰 추가, 크기를 정해주지 않으면 초록색 사각형과 동일 크기
  }
}

스크린샷 2021-12-31 오전 11 42 14

Background

  • background 수식어는 overlay 와 마찬가지로 뷰 원본의 공간을 기준으로 뷰를 중첩하는것은 같지만, 위가 아니라 그 아래 방향으로 하나씩 쌓아 나간다는 점이 다릅니다

  • overlay 는 위로, background 는 아래로 쌓인다는 점을 유의 해야 합니다

// background 는 뷰의 하위 계층에 중첩해서 쌓는 수식어 입니다.
struct Example02: View {
  var body: some View {
    Rectangle().fill(Color.yellow).frame(width: 150, height: 150, alignment: .center)
      .background(Rectangle().fill(Color.green).offset(x: 20, y: 20)) // 노란색 사각형 아래에 새로운 뷰 추가
  }
}

스크린샷 2021-12-31 오전 11 55 32

Alignment

  • overlay 와 background 는 공통으로 alignment parameter 가 있어서 추가되는 뷰의 위치를 설정해 줄 수 있습니다.
// alignment 를 이용해 뷰의 위치를 결정할 수 있습니다.
struct Example03: View {
  var body: some View {
    Circle().fill(Color.yellow.opacity(0.8)).frame(width: 250, height: 250, alignment: .center)
    // overlay
      .overlay(Text("Joystick").font(.largeTitle))
      .overlay(Image(systemName: "arrow.up").font(.title).padding(), alignment: .top)
      .overlay(Image(systemName: "arrow.left").font(.title).padding(), alignment: .leading)
      .overlay(Image(systemName: "arrow.up.right.circle.fill").font(.title), alignment: .topTrailing)
    // background
      .background(Image(systemName: "arrow.down").font(.title).padding(), alignment: .bottom)
      .background(Image(systemName: "arrow.right").font(.title).padding(), alignment: .trailing)
  }
}

스크린샷 2021-12-31 오후 1 51 53

  • ZStack 을 사용해서 위의 그림과 똑같이 그릴 수 있습니다
// ZStack 을 사용해서 위의 그림과 똑같이 그리기
struct Example04: View {
  var body: some View {
    ZStack {
      VStack {
        Spacer()
        Image(systemName: "arrow.down").font(.title).padding()
      }
      HStack {
        Spacer()
        Image(systemName: "arrow.right").font(.title).padding()
      }
      Circle()
        .fill(Color.yellow.opacity(0.8))
        .frame(width: 250, height: 250)
      Text("JoyStick").font(.largeTitle)

      ZStack(alignment: .topTrailing) {
        Color.clear
        Image(systemName: "arrow.up.right.circle.fill").font(.title)
      }
      VStack {
        Image(systemName: "arrow.up").font(.title).padding()
        Spacer()
      }
      HStack {
        Image(systemName: "arrow.left").font(.title).padding()
        Spacer()
      }
    }.frame(width: 250, height: 250, alignment: .center)
  }
}

.overlay/.background vs ZStack

📌 .overlay/.background: 전체 화면의 레이아웃을 구성 할때 사용되기 보다 UI 의 각 부분을 구성하는 개별적인 뷰 객체들을 꾸밀 때 활용됩니다

📌 ZStack: 상대적으로 직접적인 연관성이 없는 뷰들을 계층 구조로 나열하여 UI 를 구성할 때 사용합니다. 특정 콘텐츠의 변경 사항이 다른 뷰까지 함께 영향을 줄 수가 있다는 것을 유의 해야 합니다


🔶 🔷 📌 🔑 👉

🗃 Reference

Views and Controls - https://developer.apple.com/documentation/SwiftUI/Views-and-Controls

스윗한 SwiftUI - https://book.jacobko.info/#/book/1190014815

Categories:

Updated:

Leave a comment